fix(FE): resolve conflit merge development

This commit is contained in:
randy-ar
2025-12-10 13:51:19 +07:00
20 changed files with 580 additions and 136 deletions
+9
View File
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr'; import useSWR from 'swr';
import ClosingDetail from '@/components/pages/closing/ClosingDetail'; import ClosingDetail from '@/components/pages/closing/ClosingDetail';
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -19,6 +20,11 @@ const ClosingDetailPage = () => {
(id: number) => ClosingApi.getGeneralInfo(id) (id: number) => ClosingApi.getGeneralInfo(id)
); );
const { data: salesReport, isLoading: isLoadingSalesReport } = useSWR(
closingId,
(id: number) => ClosingApi.getPenjualan(id)
);
if (!closingId) { if (!closingId) {
router.back(); router.back();
@@ -43,6 +49,9 @@ const ClosingDetailPage = () => {
{!isLoadingClosing && isResponseSuccess(closing) && ( {!isLoadingClosing && isResponseSuccess(closing) && (
<ClosingDetail id={Number(closingId)} initialValue={closing.data} /> <ClosingDetail id={Number(closingId)} initialValue={closing.data} />
)} )}
{!isLoadingSalesReport && isResponseSuccess(salesReport) && (
<SalesReportTable type='detail' initialValues={salesReport.data} />
)}
</div> </div>
); );
}; };
@@ -52,6 +52,7 @@ export default function ProjectFlockLayout({
closeOnBackdropClick={isDetail ? true : false} closeOnBackdropClick={isDetail ? true : false}
onBackdropClick={handleBackdropClick} onBackdropClick={handleBackdropClick}
variant='right' variant='right'
zIndex='99999'
sidebarContent={isOpen && <div className=''>{children}</div>} sidebarContent={isOpen && <div className=''>{children}</div>}
/> />
</> </>
+1 -1
View File
@@ -54,7 +54,7 @@ const FloatingActionsButton = ({
<div <div
className={cn( className={cn(
`absolute ${positionStyles} inset-x-1/2 -translate-x-1/2 z-50`, `absolute ${positionStyles} inset-x-1/2 -translate-x-1/2 z-50`,
'mx-auto w-full max-w-lg sm:mx-0 bg-base-300 p-4 rounded-xl shadow-md transition-all duration-300 transform', 'mx-auto w-full max-w-sm sm:mx-0 bg-base-300 p-4 rounded-xl shadow-md transition-all duration-300 transform',
'bg-slate-950 backdrop-blur-md' 'bg-slate-950 backdrop-blur-md'
)} )}
> >
+13 -12
View File
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Dropdown from '@/components/dropdown/Dropdown';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { AuthApi } from '@/services/api/auth'; import { AuthApi } from '@/services/api/auth';
@@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
</div> </div>
<div className='flex gap-2'> <div className='flex gap-2'>
<div className='dropdown dropdown-end'> <Dropdown
<div position='bottom-end'
tabIndex={0} trigger={
role='button' <div className='btn btn-ghost btn-circle avatar'>
className='btn btn-ghost btn-circle avatar' <div className='w-10 rounded-full border flex justify-center items-center'>
> <Icon icon='uil:user' width={40} height={40} />
<div className='w-10 rounded-full border grid place-items-center'> </div>
<Icon icon='uil:user' width={40} height={40} />
</div> </div>
</div> }
contentClassName='w-52 mt-3'
<Menu className='dropdown-content w-52 mt-3 p-2 bg-base-100 shadow rounded-box menu-sm'> >
<Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'>
<MenuItem title='Logout' onClick={logoutClickHandler} /> <MenuItem title='Logout' onClick={logoutClickHandler} />
</Menu> </Menu>
</div> </Dropdown>
</div> </div>
</div> </div>
); );
+116
View File
@@ -0,0 +1,116 @@
'use client';
import { ReactNode, useRef, useEffect, useState } from 'react';
import { cn } from '@/lib/helper';
interface DropdownProps {
trigger: ReactNode;
children: ReactNode;
position?:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end';
align?: 'start' | 'center' | 'end';
hover?: boolean;
className?: string;
contentClassName?: string;
}
const Dropdown = ({
trigger,
children,
position = 'bottom',
align = 'start',
hover = false,
className,
contentClassName,
}: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// Build position classes
const getPositionClasses = () => {
const classes: string[] = [];
// Handle combined positions like 'top-start'
if (position.includes('-')) {
const [pos, al] = position.split('-');
classes.push(`dropdown-${pos}`);
classes.push(`dropdown-${al}`);
} else {
classes.push(`dropdown-${position}`);
if (align !== 'start') {
classes.push(`dropdown-${align}`);
}
}
return classes.join(' ');
};
const handleToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// alert('clicked');
setIsOpen(!isOpen);
};
return (
<div
ref={dropdownRef}
className={cn(
'dropdown',
getPositionClasses(),
hover && 'dropdown-hover',
isOpen && 'dropdown-open',
className
)}
>
{/* Trigger Button */}
<div onClick={handleToggle} className='cursor-pointer'>
{trigger}
</div>
{/* Dropdown Content - Only render when open */}
{isOpen && (
<div
tabIndex={-1}
className={cn('dropdown-content z-[10]', contentClassName)}
onClick={() => setIsOpen(false)} // Close on item click
>
{children}
</div>
)}
</div>
);
};
export default Dropdown;
@@ -0,0 +1,285 @@
'use client';
import React, { useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Badge from '@/components/Badge';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import { BaseClosingSales, BaseSales } 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';
interface SalesReportTableProps {
type?: 'detail';
initialValues?: BaseClosingSales;
}
const SalesReportTable = ({
type = 'detail',
initialValues,
}: SalesReportTableProps) => {
const salesData: BaseSales[] = useMemo(() => {
return initialValues?.sales || [];
}, [initialValues]);
const totals = useMemo(() => {
if (salesData.length === 0) {
return {
totalQuantity: 0,
totalWeight: 0,
avgWeight: 0,
avgPricePartner: 0,
totalPartner: 0,
};
}
const totalQuantity = salesData.reduce(
(sum, item) => sum + (item.qty || 0),
0
);
const totalWeight = salesData.reduce(
(sum, item) => sum + (item.weight || 0),
0
);
const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0;
const validPriceItems = salesData.filter(
(item) => item.price != null && item.price > 0
);
const avgPricePartner =
validPriceItems.length > 0
? validPriceItems.reduce((sum, item) => sum + item.price, 0) /
validPriceItems.length
: 0;
const totalPartner = salesData.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
return {
totalQuantity,
totalWeight,
avgWeight,
avgPricePartner,
totalPartner,
};
}, [salesData]);
const salesColumns: ColumnDef<BaseSales>[] = useMemo(
() => [
{
id: 'realization_date',
accessorKey: 'realization_date',
header: 'Tanggal Realisasi',
cell: (props) => {
const date = props.row.original.realization_date;
return date ? formatDate(date, 'DD MMM YYYY') : '-';
},
footer: () => (
<div className='font-semibold text-gray-900'>Total Penjualan</div>
),
},
{
id: 'age',
accessorKey: 'age',
header: 'Umur',
cell: (props) => props.getValue() || '-',
},
{
id: 'do_number',
accessorKey: 'do_number',
header: 'No. DO',
cell: (props) => props.getValue() || '-',
},
{
id: 'product',
accessorKey: 'product',
header: 'Produk',
cell: (props) => {
const product = props.getValue() as Product;
return product?.name || '-';
},
},
{
id: 'customer',
accessorKey: 'customer',
header: 'Customer',
cell: (props) => {
const customer = props.getValue() as Customer;
return customer?.name || '-';
},
},
{
id: 'jumlah',
header: 'Jumlah',
columns: [
{
id: 'qty',
accessorKey: 'qty',
header: 'Kuantitas',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-left'>{formatNumber(value)}</div>;
},
footer: () => (
<div className='text-left font-semibold text-gray-900'>
{formatNumber(totals.totalQuantity)}
</div>
),
},
{
id: 'weight',
accessorKey: 'weight',
header: 'Kg',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-left'>{formatNumber(value)}</div>;
},
footer: () => (
<div className='text-left font-semibold text-gray-900'>
{formatNumber(totals.totalWeight)}
</div>
),
},
],
},
{
id: 'avg_weight',
accessorKey: 'avg_weight',
header: 'AVG (Kg)',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-left'>{formatNumber(value)}</div>;
},
footer: () => (
<div className='text-left font-semibold text-gray-900'>
{formatNumber(totals.avgWeight)}
</div>
),
},
{
id: 'price_partner',
accessorKey: 'price',
header: 'Harga Mitra (Rp)',
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.avgPricePartner)}
</div>
),
},
{
id: 'total_mitra',
accessorKey: 'total_price',
header: 'Total Mitra (Rp)',
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.totalPartner)}
</div>
),
},
{
id: 'price_act',
accessorKey: 'price',
header: 'Harga Act (Rp)',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
},
{
id: 'total_act',
accessorKey: 'total_price',
header: 'Total Act (Rp)',
cell: (props) => {
const value = props.getValue() as number;
return <div className='text-right'>{formatCurrency(value)}</div>;
},
},
{
id: 'kandang',
accessorKey: 'kandang',
header: 'Kandang',
cell: (props) => {
const kandang = props.getValue() as Kandang;
return kandang?.name || '-';
},
},
{
id: 'payment_status',
accessorKey: 'payment_status',
header: 'Status Pembayaran',
cell: (props) => {
const status = props.getValue() as string;
const getStatusColor = (status: string) => {
if (!status) return 'neutral';
switch (status.toLowerCase()) {
case 'paid':
return 'success';
case 'tempo':
return 'warning';
default:
return 'neutral';
}
};
return (
<Badge variant='soft' size='sm' color={getStatusColor(status)}>
{status || '-'}
</Badge>
);
},
},
],
[]
);
return (
<>
<section className='w-full'>
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
<Card
className={{
wrapper: 'w-full bg-base-100',
body: 'p-0',
}}
>
<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 last:flex last:flex-row last:justify-end 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>
</>
);
};
export default SalesReportTable;
@@ -207,7 +207,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0; let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach( kandangExpense.pengajuans?.forEach(
(item) => (expenseGrandTotal += item.total_price) (item) => (expenseGrandTotal += item.price)
); );
return ( return (
@@ -238,7 +238,7 @@ const ExpenseRealizationContent = ({
<tr key={pengajuanIdx}> <tr key={pengajuanIdx}>
<td>{pengajuanItem.nonstock.name}</td> <td>{pengajuanItem.nonstock.name}</td>
<td>{pengajuanItem.qty}</td> <td>{pengajuanItem.qty}</td>
<td>{formatCurrency(pengajuanItem.total_price)}</td> <td>{formatCurrency(pengajuanItem.price)}</td>
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td> <td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
</tr> </tr>
) )
@@ -269,7 +269,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0; let expenseGrandTotal = 0;
kandangExpense.realisasi?.forEach( kandangExpense.realisasi?.forEach(
(item) => (expenseGrandTotal += item.total_price) (item) => (expenseGrandTotal += item.price)
); );
return ( return (
@@ -300,7 +300,7 @@ const ExpenseRealizationContent = ({
<tr key={realisasiIdx}> <tr key={realisasiIdx}>
<td>{realisasiItem.nonstock.name}</td> <td>{realisasiItem.nonstock.name}</td>
<td>{realisasiItem.qty}</td> <td>{realisasiItem.qty}</td>
<td>{formatCurrency(realisasiItem.total_price)}</td> <td>{formatCurrency(realisasiItem.price)}</td>
<td className='w-xs'>{realisasiItem.note ?? '-'}</td> <td className='w-xs'>{realisasiItem.note ?? '-'}</td>
</tr> </tr>
) )
@@ -402,7 +402,10 @@ const ExpenseRequestContent = ({
<th>Tanggal Transaksi</th> <th>Tanggal Transaksi</th>
<th>:</th> <th>:</th>
<td> <td>
{formatDate(initialValues?.expense_date, 'DD MMMM YYYY')} {formatDate(
initialValues?.transaction_date,
'DD MMMM YYYY'
)}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -529,7 +532,7 @@ const ExpenseRequestContent = ({
let expenseGrandTotal = 0; let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach( kandangExpense.pengajuans?.forEach(
(item) => (expenseGrandTotal += item.total_price) (item) => (expenseGrandTotal += item.price)
); );
return ( return (
@@ -550,7 +553,7 @@ const ExpenseRequestContent = ({
<tr> <tr>
<th>Nonstock</th> <th>Nonstock</th>
<th>Total Kuantitas</th> <th>Total Kuantitas</th>
<th>Total Biaya</th> <th>Harga Satuan</th>
<th>Catatan</th> <th>Catatan</th>
</tr> </tr>
</thead> </thead>
@@ -560,9 +563,7 @@ const ExpenseRequestContent = ({
<tr key={pengajuanIdx}> <tr key={pengajuanIdx}>
<td>{pengajuanItem.nonstock.name}</td> <td>{pengajuanItem.nonstock.name}</td>
<td>{pengajuanItem.qty}</td> <td>{pengajuanItem.qty}</td>
<td> <td>{formatCurrency(pengajuanItem.price)}</td>
{formatCurrency(pengajuanItem.total_price)}
</td>
<td className='w-xs'> <td className='w-xs'>
{pengajuanItem.note ?? '-'} {pengajuanItem.note ?? '-'}
</td> </td>
@@ -263,11 +263,11 @@ const ExpensesTable = () => {
}, },
}, },
{ {
accessorKey: 'expense_date', accessorKey: 'transaction_date',
header: 'Tanggal Pengajuan', header: 'Tanggal Pengajuan',
cell: (props) => cell: (props) =>
props.row.original.expense_date props.row.original.transaction_date
? formatDate(props.row.original.expense_date, 'DD MMM YYYY') ? formatDate(props.row.original.transaction_date, 'DD MMM YYYY')
: '-', : '-',
}, },
{ {
@@ -27,7 +27,7 @@ type ExpenseRealizationFormSchemaType = {
label: string; label: string;
}; };
quantity?: number; quantity?: number;
total_cost?: number; price?: number;
notes?: string; notes?: string;
}[]; }[];
}[]; }[];
@@ -82,7 +82,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
label: Yup.string().required(), label: Yup.string().required(),
}).required('Nonstock wajib diisi!'), }).required('Nonstock wajib diisi!'),
quantity: Yup.number().required('Total kuantitas wajib diisi!'), quantity: Yup.number().required('Total kuantitas wajib diisi!'),
total_cost: Yup.number().required('Total biaya wajib diisi!'), price: Yup.number().required('Harga satuan wajib diisi!'),
notes: Yup.string(), notes: Yup.string(),
}) })
) )
@@ -155,7 +155,7 @@ export const getExpenseRealizationFormInitialValues = (
label: realisasiItem.nonstock.name, label: realisasiItem.nonstock.name,
}, },
quantity: realisasiItem.qty, quantity: realisasiItem.qty,
total_cost: realisasiItem.total_price, price: realisasiItem.price,
notes: realisasiItem.note, notes: realisasiItem.note,
}; };
}) })
@@ -166,7 +166,7 @@ export const getExpenseRealizationFormInitialValues = (
label: expenseItem.nonstock.name, label: expenseItem.nonstock.name,
}, },
quantity: expenseItem.qty, quantity: expenseItem.qty,
total_cost: expenseItem.total_price, price: expenseItem.price,
notes: expenseItem.note, notes: expenseItem.note,
})) }))
: []; : [];
@@ -98,15 +98,10 @@ const ExpenseRealizationForm = ({
values.realizations.forEach((realization) => { values.realizations.forEach((realization) => {
realization.cost_items.forEach((costItem) => { realization.cost_items.forEach((costItem) => {
const unitPrice =
parseFloat(String(costItem.total_cost)) /
parseFloat(String(costItem.quantity));
const realizationItem = { const realizationItem = {
expense_nonstock_id: costItem.nonstock?.value as number, expense_nonstock_id: costItem.nonstock?.value as number,
qty: parseFloat(String(costItem.quantity)) as number, qty: parseFloat(String(costItem.quantity)) as number,
unit_price: unitPrice, price: parseFloat(String(costItem.price)) as number,
total_price: parseFloat(String(costItem.total_cost)) as number,
notes: costItem.notes ?? '', notes: costItem.notes ?? '',
}; };
@@ -177,7 +172,7 @@ const ExpenseRealizationForm = ({
{ {
nonstock: undefined, nonstock: undefined,
quantity: undefined, quantity: undefined,
total_cost: undefined, price: undefined,
notes: '', notes: '',
}, },
], ],
@@ -48,7 +48,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
}; };
const isExpenseRepeaterInputError = ( const isExpenseRepeaterInputError = (
column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', column: 'nonstock' | 'quantity' | 'price' | 'notes',
kandangExpenseIdx: number, kandangExpenseIdx: number,
expenseIdx: number expenseIdx: number
) => { ) => {
@@ -112,7 +112,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
<tr> <tr>
<th>Nonstock</th> <th>Nonstock</th>
<th>Total Kuantitas</th> <th>Total Kuantitas</th>
<th>Total Biaya</th> <th>Harga Satuan</th>
<th>Catatan</th> <th>Catatan</th>
</tr> </tr>
</thead> </thead>
@@ -163,17 +163,17 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
<td className='p-2'> <td className='p-2'>
<NumberInput <NumberInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].total_cost`} name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Total Biaya' placeholder='Masukkan Harga Satuan'
value={ value={
formik.values.realizations[ formik.values.realizations[
kandangExpenseIdx kandangExpenseIdx
].cost_items[expenseIdx].total_cost ?? '' ].cost_items[expenseIdx].price ?? ''
} }
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError( isError={isExpenseRepeaterInputError(
'total_cost', 'price',
kandangExpenseIdx, kandangExpenseIdx,
expenseIdx expenseIdx
)} )}
@@ -20,7 +20,7 @@ type ExpenseFormSchemaType = {
existing_documents?: { id: number; name: string; url: string }[]; existing_documents?: { id: number; name: string; url: string }[];
deleted_documents?: number[]; deleted_documents?: number[];
documents?: File[]; documents?: File[];
cost_per_kandangs: { expense_nonstocks: {
kandang_id: number; kandang_id: number;
cost_items: { cost_items: {
nonstock?: { nonstock?: {
@@ -28,7 +28,7 @@ type ExpenseFormSchemaType = {
label: string; label: string;
}; };
quantity?: number; quantity?: number;
total_cost?: number; price?: number;
notes?: string; notes?: string;
}[]; }[];
}[]; }[];
@@ -74,7 +74,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
documents: Yup.array().of(Yup.mixed<File>().required()).optional(), documents: Yup.array().of(Yup.mixed<File>().required()).optional(),
cost_per_kandangs: Yup.array() expense_nonstocks: Yup.array()
.of( .of(
Yup.object({ Yup.object({
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(), kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(),
@@ -86,7 +86,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
label: Yup.string().required(), label: Yup.string().required(),
}).required('Nonstock wajib diisi!'), }).required('Nonstock wajib diisi!'),
quantity: Yup.number().required('Total kuantitas wajib diisi!'), quantity: Yup.number().required('Total kuantitas wajib diisi!'),
total_cost: Yup.number().required('Total biaya wajib diisi!'), price: Yup.number().required('Harga satuan wajib diisi!'),
notes: Yup.string(), notes: Yup.string(),
}) })
) )
@@ -128,8 +128,8 @@ export const getExpenseFormInitialValues = (
label: initialValues.location.name, label: initialValues.location.name,
} }
: undefined, : undefined,
transaction_date: initialValues?.expense_date transaction_date: initialValues?.transaction_date
? formatDate(initialValues.expense_date, 'YYYY-MM-DD') ? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
: undefined, : undefined,
kandangs: initialValues?.kandangs.map((kandang) => ({ kandangs: initialValues?.kandangs.map((kandang) => ({
id: kandang.kandang_id, id: kandang.kandang_id,
@@ -148,7 +148,7 @@ export const getExpenseFormInitialValues = (
})), })),
deleted_documents: [], deleted_documents: [],
documents: [], documents: [],
cost_per_kandangs: initialValues?.kandangs expense_nonstocks: initialValues?.kandangs
? initialValues.kandangs.map((kandangExpense) => ({ ? initialValues.kandangs.map((kandangExpense) => ({
kandang_id: kandangExpense.kandang_id, kandang_id: kandangExpense.kandang_id,
cost_items: kandangExpense.pengajuans cost_items: kandangExpense.pengajuans
@@ -158,7 +158,7 @@ export const getExpenseFormInitialValues = (
label: expenseItem.nonstock.name, label: expenseItem.nonstock.name,
}, },
quantity: expenseItem.qty, quantity: expenseItem.qty,
total_cost: expenseItem.total_price, price: expenseItem.price,
notes: expenseItem.note, notes: expenseItem.note,
})) }))
: [], : [],
@@ -110,12 +110,12 @@ const ExpenseRequestForm = ({
transaction_date: values?.transaction_date as string, transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number, supplier_id: values.supplier?.value as number,
documents: values.documents as File[], documents: values.documents as File[],
cost_per_kandangs: values.cost_per_kandangs.map((costPerKandang) => ({ expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({
kandang_id: costPerKandang.kandang_id, kandang_id: expenseNonstock.kandang_id,
cost_items: costPerKandang.cost_items.map((costItem) => ({ cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number, nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number, quantity: parseFloat(String(costItem.quantity)) as number,
total_cost: parseFloat(String(costItem.total_cost)) as number, price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '', notes: costItem.notes ?? '',
})), })),
})), })),
@@ -132,13 +132,13 @@ const ExpenseRequestForm = ({
transaction_date: values?.transaction_date as string, transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number, supplier_id: values.supplier?.value as number,
documents: values.documents as File[], documents: values.documents as File[],
cost_per_kandang: values.cost_per_kandangs.map( expense_nonstocks: values.expense_nonstocks.map(
(costPerKandang) => ({ (expenseNonstock) => ({
kandang_id: costPerKandang.kandang_id, kandang_id: expenseNonstock.kandang_id,
cost_items: costPerKandang.cost_items.map((costItem) => ({ cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number, nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number, quantity: parseFloat(String(costItem.quantity)) as number,
total_cost: parseFloat(String(costItem.total_cost)) as number, price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '', notes: costItem.notes ?? '',
})), })),
}) })
@@ -179,53 +179,54 @@ const ExpenseRequestForm = ({
formik.setFieldValue('location', val); formik.setFieldValue('location', val);
formik.setFieldValue('kandangs', []); formik.setFieldValue('kandangs', []);
formik.setFieldValue('cost_per_kandangs', []); formik.setFieldValue('expense_nonstocks', []);
}; };
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
formik.setFieldTouched('kandangs', true); formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs); formik.setFieldValue('kandangs', kandangs);
const newCostPerKandangs = [...(formik.values.cost_per_kandangs ?? [])]; const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])];
// add new cost_per_kandangs // add new expense_nonstocks
kandangs.forEach((kandangItem) => { kandangs.forEach((kandangItem) => {
const isKandangExistInCostPerKandangs = newCostPerKandangs.find( const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find(
(costPerKandangItem) => costPerKandangItem.kandang_id === kandangItem.id (expenseNonstockItem) =>
expenseNonstockItem.kandang_id === kandangItem.id
); );
if (isKandangExistInCostPerKandangs) return; if (isKandangExistInExpenseNonstocks) return;
newCostPerKandangs.push({ newExpenseNonstocks.push({
kandang_id: kandangItem.id, kandang_id: kandangItem.id,
cost_items: [ cost_items: [
{ {
nonstock: undefined, nonstock: undefined,
quantity: undefined, quantity: undefined,
total_cost: undefined, price: undefined,
notes: '', notes: '',
}, },
], ],
}); });
}); });
// prune cost_per_kandangs // prune expense_nonstocks
const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
const deletedCostPerKandangsIdx: number[] = []; const deletedExpenseNonstocksIdx: number[] = [];
newCostPerKandangs.forEach((costPerKandang, idx) => { newExpenseNonstocks.forEach((expenseNonstock, idx) => {
const isCostPerKandangValid = kandangIds.has(costPerKandang.kandang_id); const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id);
if (!isCostPerKandangValid) { if (!isExpenseNonstockValid) {
deletedCostPerKandangsIdx.push(idx); deletedExpenseNonstocksIdx.push(idx);
} }
}); });
deletedCostPerKandangsIdx.forEach((deletedCostPerKandangIdx) => { deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => {
newCostPerKandangs.splice(deletedCostPerKandangIdx, 1); newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1);
}); });
formik.setFieldValue('cost_per_kandangs', newCostPerKandangs); formik.setFieldValue('expense_nonstocks', newExpenseNonstocks);
}; };
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -41,28 +41,28 @@ const ExpenseRequestKandangDetailExpense: React.FC<
val: OptionType | OptionType[] | null val: OptionType | OptionType[] | null
) => { ) => {
formik.setFieldTouched( formik.setFieldTouched(
`cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
true true
); );
formik.setFieldValue( formik.setFieldValue(
`cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
val val
); );
}; };
const addExpenseItemHandler = (kandangExpenseIdx: number) => { const addExpenseItemHandler = (kandangExpenseIdx: number) => {
const newExpensesValue = [ const newExpensesValue = [
...formik.values.cost_per_kandangs[kandangExpenseIdx].cost_items, ...formik.values.expense_nonstocks[kandangExpenseIdx].cost_items,
{ {
nonstock: undefined, nonstock: undefined,
total_cost: undefined, price: undefined,
quantity: undefined, quantity: undefined,
notes: '', notes: '',
}, },
]; ];
formik.setFieldValue( formik.setFieldValue(
`cost_per_kandangs[${kandangExpenseIdx}].cost_items`, `expense_nonstocks[${kandangExpenseIdx}].cost_items`,
newExpensesValue newExpensesValue
); );
}; };
@@ -71,28 +71,28 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx: number, kandangExpenseIdx: number,
expenseIdx: number expenseIdx: number
) => { ) => {
const path = `cost_per_kandangs[${kandangExpenseIdx}].cost_items`; const path = `expense_nonstocks[${kandangExpenseIdx}].cost_items`;
// trims values, errors, and touched at expenseIdx // trims values, errors, and touched at expenseIdx
removeArrayItemAndSync(formik, path, expenseIdx); removeArrayItemAndSync(formik, path, expenseIdx);
}; };
const isExpenseRepeaterInputError = ( const isExpenseRepeaterInputError = (
column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', column: 'nonstock' | 'quantity' | 'price' | 'notes',
kandangExpenseIdx: number, kandangExpenseIdx: number,
expenseIdx: number expenseIdx: number
) => { ) => {
return ( return (
formik.touched.cost_per_kandangs?.[kandangExpenseIdx]?.cost_items?.[ formik.touched.expense_nonstocks?.[kandangExpenseIdx]?.cost_items?.[
expenseIdx expenseIdx
]?.[column] && ]?.[column] &&
Boolean( Boolean(
formik.errors.cost_per_kandangs?.[kandangExpenseIdx] instanceof formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
Object && Object &&
formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx expenseIdx
] instanceof Object && ] instanceof Object &&
formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx expenseIdx
]?.[column] ]?.[column]
) )
@@ -113,7 +113,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
</div> </div>
<div className='w-full flex flex-col gap-6'> <div className='w-full flex flex-col gap-6'>
{(formik.values.cost_per_kandangs.length === 0 || {(formik.values.expense_nonstocks.length === 0 ||
!formik.values.supplier?.value) && ( !formik.values.supplier?.value) && (
<div> <div>
<p className='text-sm text-gray-400 text-center'> <p className='text-sm text-gray-400 text-center'>
@@ -122,9 +122,9 @@ const ExpenseRequestKandangDetailExpense: React.FC<
</div> </div>
)} )}
{formik.values.cost_per_kandangs.length > 0 && {formik.values.expense_nonstocks.length > 0 &&
formik.values.supplier?.value && formik.values.supplier?.value &&
formik.values.cost_per_kandangs.map( formik.values.expense_nonstocks.map(
(kandangExpense, kandangExpenseIdx) => { (kandangExpense, kandangExpenseIdx) => {
const kandangName = formik.values.kandangs?.find( const kandangName = formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id (kandang) => kandang.id === kandangExpense.kandang_id
@@ -147,7 +147,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
<tr> <tr>
<th>Nonstock</th> <th>Nonstock</th>
<th>Total Kuantitas</th> <th>Total Kuantitas</th>
<th>Total Biaya</th> <th>Harga Satuan</th>
<th>Catatan</th> <th>Catatan</th>
{type !== 'detail' && <th>Aksi</th>} {type !== 'detail' && <th>Aksi</th>}
</tr> </tr>
@@ -178,10 +178,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
<td className='p-2'> <td className='p-2'>
<NumberInput <NumberInput
required required
name={`cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`} name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
placeholder='Masukkan Total Kuantitas' placeholder='Masukkan Total Kuantitas'
value={ value={
formik.values.cost_per_kandangs[ formik.values.expense_nonstocks[
kandangExpenseIdx kandangExpenseIdx
].cost_items[expenseIdx].quantity ?? '' ].cost_items[expenseIdx].quantity ?? ''
} }
@@ -198,18 +198,17 @@ const ExpenseRequestKandangDetailExpense: React.FC<
<td className='p-2'> <td className='p-2'>
<NumberInput <NumberInput
name={`cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].total_cost`} name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Total Biaya' placeholder='Masukkan Harga Satuan'
value={ value={
formik.values.cost_per_kandangs[ formik.values.expense_nonstocks[
kandangExpenseIdx kandangExpenseIdx
].cost_items[expenseIdx].total_cost ?? ].cost_items[expenseIdx].price ?? ''
''
} }
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError( isError={isExpenseRepeaterInputError(
'total_cost', 'price',
kandangExpenseIdx, kandangExpenseIdx,
expenseIdx expenseIdx
)} )}
@@ -224,10 +223,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
<td className='p-2'> <td className='p-2'>
<TextInput <TextInput
name={`cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`} name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
placeholder='Tuliskan catatan' placeholder='Tuliskan catatan'
value={ value={
formik.values.cost_per_kandangs[ formik.values.expense_nonstocks[
kandangExpenseIdx kandangExpenseIdx
].cost_items[expenseIdx].notes ?? '' ].cost_items[expenseIdx].notes ?? ''
} }
@@ -224,7 +224,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
{ label: 'Vendor', value: expense?.supplier.name }, { label: 'Vendor', value: expense?.supplier.name },
{ {
label: 'Tanggal Transaksi', label: 'Tanggal Transaksi',
value: formatDate(expense?.expense_date, 'DD MMMM YYYY'), value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
}, },
{ {
label: 'Tanggal Realisasi', label: 'Tanggal Realisasi',
@@ -326,7 +326,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
let expenseRequestTotal = 0; let expenseRequestTotal = 0;
kandangExpense.pengajuans?.forEach( kandangExpense.pengajuans?.forEach(
(item) => (expenseRequestTotal += item.total_price) (item) => (expenseRequestTotal += item.price)
); );
return ( return (
@@ -374,7 +374,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
<Text <Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText} style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
> >
Total Biaya Harga Satuan
</Text> </Text>
</View> </View>
<View <View
@@ -424,7 +424,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
]} ]}
> >
<Text style={ExpensePDFStyle.kandangExpenseLabelText}> <Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(pengajuan.total_price)} {formatCurrency(pengajuan.price)}
</Text> </Text>
</View> </View>
<View <View
@@ -484,7 +484,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
let expenseRealizationTotal = 0; let expenseRealizationTotal = 0;
kandangExpense.realisasi?.forEach( kandangExpense.realisasi?.forEach(
(item) => (expenseRealizationTotal += item.total_price) (item) => (expenseRealizationTotal += item.price)
); );
return ( return (
@@ -532,7 +532,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
<Text <Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText} style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
> >
Total Biaya Harga Satuan
</Text> </Text>
</View> </View>
<View <View
@@ -582,7 +582,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
]} ]}
> >
<Text style={ExpensePDFStyle.kandangExpenseLabelText}> <Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(realisasi.total_price)} {formatCurrency(realisasi.price)}
</Text> </Text>
</View> </View>
<View <View
+17 -1
View File
@@ -20,6 +20,7 @@ import {
dummyGetOverhead, dummyGetOverhead,
} from '@/dummy/closing.dummy'; } from '@/dummy/closing.dummy';
import { httpClient, httpClientFetcher } from '@/services/http/client'; import { httpClient, httpClientFetcher } from '@/services/http/client';
import { ClosingSales } from '@/types/api/closing';
export class ClosingApiService extends BaseApiService<Closing, null, null> { export class ClosingApiService extends BaseApiService<Closing, null, null> {
constructor(basePath: string) { constructor(basePath: string) {
@@ -53,6 +54,21 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
return getSingleRes; return getSingleRes;
} catch (error) { } catch (error) {
if (axios.isAxiosError<BaseApiResponse<Closing>>(error)) { if (axios.isAxiosError<BaseApiResponse<Closing>>(error)) {
}
return undefined;
}
}
async getPenjualan(
id: number
): Promise<BaseApiResponse<ClosingSales> | undefined> {
try {
const getPenjualanPath = `${id}/penjualan`;
return await this.customRequest<BaseApiResponse<ClosingSales>>(
getPenjualanPath
);
} catch (error) {
if (axios.isAxiosError<BaseApiResponse<ClosingSales>>(error)) {
return error.response?.data; return error.response?.data;
} }
return undefined; return undefined;
@@ -178,4 +194,4 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
} }
} }
export const ClosingApi = new ClosingApiService('/closing'); export const ClosingApi = new ClosingApiService('/closings');
+4 -4
View File
@@ -492,8 +492,8 @@ export class ExpenseApiService extends BaseApiService<
}); });
formData.append( formData.append(
'cost_per_kandangs', 'expense_nonstocks',
JSON.stringify(payload.cost_per_kandangs) JSON.stringify(payload.expense_nonstocks)
); );
return formData; return formData;
@@ -514,8 +514,8 @@ export class ExpenseApiService extends BaseApiService<
}); });
formData.append( formData.append(
'cost_per_kandang', 'expense_nonstocks',
JSON.stringify(payload.cost_per_kandang) JSON.stringify(payload.expense_nonstocks)
); );
return formData; return formData;
+28 -2
View File
@@ -1,9 +1,34 @@
import { Area } from '@/types/api/master-data/area'; import { Area } from '@/types/api/master-data/area';
import { Fcr } from '@/types/api/master-data/fcr'; import { Fcr } from '@/types/api/master-data/fcr';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { Kandang } from '@/types/api/master-data/kandang';
import { Location } from '@/types/api/master-data/location'; import { Location } from '@/types/api/master-data/location';
import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; import { Kandang } from '@/types/api/master-data/kandang';
import { Product } from '@type/api/master-data/product';
import { Customer } from '@type/api/master-data/customer';
import { BaseMetadata } from '@/types/api/api-general';
export type BaseSales = {
id: number;
realization_date: string;
age: number;
do_number: string;
product: Product;
customer: Customer;
qty: number;
weight: number;
avg_weight: number;
price: number;
total_price: number;
kandang: Kandang;
payment_status: string;
};
export type BaseClosingSales = {
project_type: string;
flock_id: number;
period: number;
sales: BaseSales[];
};
export type BaseClosing = { export type BaseClosing = {
id: number; id: number;
@@ -116,3 +141,4 @@ export type OverheadTotal = {
actual_total_amount: number; actual_total_amount: number;
cost_per_bird: number; cost_per_bird: number;
}; };
export type ClosingSales = BaseMetadata & BaseClosingSales;
+14 -20
View File
@@ -18,7 +18,7 @@ export type BaseExpense = {
id: number; id: number;
path: string; path: string;
}[]; }[];
expense_date: string; transaction_date: string;
realization_date?: string; realization_date?: string;
grand_total: number; grand_total: number;
location: BaseLocation; location: BaseLocation;
@@ -29,28 +29,23 @@ export type BaseExpense = {
name: string; name: string;
pengajuans?: { pengajuans?: {
id: number; id: number;
expense_id: number;
kandang_id: number;
nonstock_id: number;
qty: number; qty: number;
unit_price: number; price: number;
total_price: number;
note?: string; note?: string;
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>; nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
project_flock_kandang: { created_at: string;
id: number;
kandang_id: number;
};
}[]; }[];
realisasi?: { realisasi?: {
id: number; id: number;
expense_nonstock_id: number;
qty: number; qty: number;
unit_price: number; price: number;
total_price: number;
date: string;
note?: string; note?: string;
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>; nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
project_flock_kandang: { created_at: string;
id: number;
kandang_id: number;
};
}[]; }[];
}[]; }[];
total_pengajuan: number; total_pengajuan: number;
@@ -65,12 +60,12 @@ export type CreateExpensePayload = {
transaction_date: string; transaction_date: string;
supplier_id: number; supplier_id: number;
documents: File[]; documents: File[];
cost_per_kandangs: { expense_nonstocks: {
kandang_id: number; kandang_id: number;
cost_items: { cost_items: {
nonstock_id: number; nonstock_id: number;
quantity: number; quantity: number;
total_cost: number; price: number;
notes: string; notes: string;
}[]; }[];
}[]; }[];
@@ -81,12 +76,12 @@ export type UpdateExpensePayload = {
transaction_date: string; transaction_date: string;
supplier_id: number; supplier_id: number;
documents: File[]; documents: File[];
cost_per_kandang: { expense_nonstocks: {
kandang_id: number; kandang_id: number;
cost_items: { cost_items: {
nonstock_id: number; nonstock_id: number;
quantity: number; quantity: number;
total_cost: number; price: number;
notes: string; notes: string;
}[]; }[];
}[]; }[];
@@ -98,8 +93,7 @@ export type CreateExpenseRealizationPayload = {
realizations: { realizations: {
expense_nonstock_id: number; expense_nonstock_id: number;
qty: number; qty: number;
unit_price: number; price: number;
total_price: number;
notes: string; notes: string;
}[]; }[];
}; };