refactor(FE): refactor UI Sales Order and Delivery Order

This commit is contained in:
randy-ar
2026-02-02 22:00:42 +07:00
parent a57f65a420
commit 17589cb2b4
15 changed files with 2338 additions and 821 deletions
@@ -7,6 +7,12 @@ import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import {
BaseDeliveryOrder,
BaseSalesOrder,
Marketing,
} from '@/types/api/marketing/marketing';
import { formatDate } from '@/lib/helper';
type MarketingSchemaType = {
customer_id: number | undefined;
@@ -64,7 +70,7 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
.required('Pengiriman wajib diisi!')
.test(
'at-least-one-valid-row',
'Minimal ada satu baris pengiriman yang valid!',
'Minimal harus ada satu baris pengiriman yang lengkap diisi!',
function (items) {
if (!items || items.length === 0) return false;
@@ -86,3 +92,124 @@ export const UpdateSalesOrderSchema = SalesOrderSchema;
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
// ================ Helper Function ================
const SalesProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
return {
id: product.id,
vehicle_number: product.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id,
kandang: {
value: product.product_warehouse.warehouse.id,
label: product.product_warehouse.warehouse.name,
},
product_warehouse: {
value: product.product_warehouse.id,
label: product.product_warehouse.product.name,
},
product_warehouse_id: product.product_warehouse.id,
unit_price: product.unit_price,
total_weight: product.total_weight,
qty: product.qty,
avg_weight: product.avg_weight,
total_price: product.total_price,
};
};
const DeliveryProductToFieldValues = (
salesOrders: BaseSalesOrder[],
delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => {
const data = delivery.deliveries.map((item) => {
const soId = salesOrders.find(
(so) => so.product_warehouse.id === item.product_warehouse.id
)?.id;
return {
id: soId,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
avg_weight: item.avg_weight,
total_price: item.total_price,
vehicle_number: item.vehicle_number,
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
do_number: delivery.do_number,
marketing_product_id: soId,
marketing_product: {
id: soId,
vehicle_number: item.vehicle_number,
kandang_id: item.product_warehouse.warehouse.id,
kandang: {
value: item.product_warehouse.warehouse.id,
label: item.product_warehouse.warehouse.name,
},
product_warehouse: {
value: item.product_warehouse.id,
label: item.product_warehouse.product.name,
},
product_warehouse_id: item.product_warehouse.id,
unit_price: item.unit_price,
total_weight: item.total_weight,
qty: item.qty,
avg_weight: item.avg_weight,
total_price: item.total_price,
},
} as DeliveryOrderProductFormValues;
});
return data;
};
export const mergeSOwithDO = (
salesOrders: SalesOrderProductFormValues[],
deliveryOrders: DeliveryOrderProductFormValues[],
autofill?: boolean
): DeliveryOrderProductFormValues[] => {
return salesOrders.map((so) => {
const delivery = deliveryOrders.find(
(d) => d?.marketing_product_id === so.id
);
return {
...so, // nilai dasar dari sales order
marketing_product_id: so.id,
delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price: autofill ? so.unit_price : delivery?.unit_price,
total_weight: autofill ? so.total_weight : delivery?.total_weight,
qty: autofill ? so.qty : delivery?.qty,
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price,
marketing_product: so, // jika ada, override
} as DeliveryOrderProductFormValues;
});
};
export const getFilledMarketingFormInitialValues = (
marketing: Marketing
): SalesOrderFormValues & DeliveryOrderFormValues => {
return {
customer_id: marketing.customer.id,
sales_person_id: marketing.sales_person.id,
sales_person: {
value: marketing.sales_person.id,
label: marketing.sales_person.name,
},
customer: {
value: marketing.customer.id,
label: marketing.customer.name,
},
so_date: formatDate(marketing.so_date, 'yyyy-MM-DD'),
notes: marketing.notes,
sales_order: marketing.sales_order.map((so) =>
SalesProductToFieldValues(so)
),
delivery_order: mergeSOwithDO(
marketing.sales_order?.map(SalesProductToFieldValues) ?? [],
marketing.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(marketing.sales_order, delivery)
) ?? [],
marketing.latest_approval.step_number > 1
),
};
};
@@ -41,7 +41,6 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
import SalesOrderProductTable from '@/components/pages/marketing/form/table-view/SalesOrderProductTable';
import SalesOrderProductForm from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm';
import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-view/DeliveryOrderProductTable';
import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
@@ -53,7 +52,6 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { CreatedUser } from '@/types/api/api-general';
import { UserApi } from '@/services/api/user';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
@@ -134,7 +132,8 @@ export const DeliveryProductToFieldValues = (
};
export const mergeSOwithDO = (
salesOrders: SalesOrderProductFormValues[],
deliveryOrders: DeliveryOrderProductFormValues[]
deliveryOrders: DeliveryOrderProductFormValues[],
autofill?: boolean
): DeliveryOrderProductFormValues[] => {
return salesOrders.map((so) => {
const delivery = deliveryOrders.find(
@@ -147,11 +146,11 @@ export const mergeSOwithDO = (
delivery_date: delivery?.delivery_date || undefined,
do_number: delivery?.do_number || undefined,
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
unit_price: delivery?.unit_price,
total_weight: delivery?.total_weight,
qty: delivery?.qty,
avg_weight: delivery?.avg_weight,
total_price: delivery?.total_price,
unit_price: autofill ? so.unit_price : delivery?.unit_price,
total_weight: autofill ? so.total_weight : delivery?.total_weight,
qty: autofill ? so.qty : delivery?.qty,
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price,
marketing_product: so, // jika ada, override
} as DeliveryOrderProductFormValues;
});
@@ -701,6 +700,7 @@ const MarketingForm = ({
}}
>
<MemoizedDeliveryOrderProductTable
marketing={initialValues}
formType={formType}
data={deliveryOrderValues}
onEdit={handleEditDO}
@@ -10,9 +10,7 @@ import NumberInput from '@/components/input/NumberInput';
import PatternInput from '@/components/input/PatternInput';
import { formatVechicleNumber } from '@/lib/helper';
import DateInput from '@/components/input/DateInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
import Badge from '@/components/Badge';
import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm';
import * as Yup from 'yup';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -20,6 +18,9 @@ import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import useSWR from 'swr';
import { ProductApi } from '@/services/api/master-data';
import StatusBadge from '@/components/helper/StatusBadge';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { OptionType } from '@/components/input/SelectInput';
const roundWeight = (value: number) => Number(value.toFixed(2));
const roundPrice = (value: number) => Math.round(value);
@@ -247,7 +248,7 @@ const DeliveryOrderProductForm = ({
return (
<>
<form
className='size-full'
className='size-full flex flex-col relative gap-1.5'
onSubmit={handleFormSubmit}
onReset={handleResetForm}
>
@@ -256,218 +257,214 @@ const DeliveryOrderProductForm = ({
<Alert color='error'>{formikErrorMessage}</Alert>
</div>
)}
<DateInput
name='delivery_date'
label='Tanggal'
value={formik.values.delivery_date ?? undefined}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.delivery_date && Boolean(formik.errors.delivery_date)
}
errorMessage={formik.errors.delivery_date}
placeholder='Pilih Tanggal'
className={{
inputWrapper: 'bg-white',
}}
required
/>
<SelectInputRadio
options={options}
label='Produk'
placeholder='Pilih Produk'
readOnly={formState == 'edit'}
value={
selectedProduct
? ({
value: selectedProduct?.value,
label: exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.product_warehouse?.label,
} as OptionType)
: null
}
onChange={(value) => {
const selected = value as OptionType;
setSelectedProduct(selected);
<div className='grid sm:grid-cols-3 gap-4'>
<SelectInput
options={options}
label='Produk'
placeholder='Pilih Produk'
isDisabled={formState == 'edit'}
value={
selectedProduct
? ({
value: selectedProduct?.value,
label: exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.product_warehouse?.label,
} as OptionType)
: null
}
onChange={(value) => {
const selected = value as OptionType;
setSelectedProduct(selected);
const so = salesOrders?.find(
(item) => item.id === selected?.value
);
if (!so) {
formik.setValues({
...formik.values,
marketing_product_id: undefined,
marketing_product: null,
qty: '',
unit_price: '',
total_price: '',
avg_weight: '',
total_weight: '',
vehicle_number: '',
});
return;
}
const so = salesOrders?.find((item) => item.id === selected?.value);
if (!so) {
formik.setValues({
...formik.values,
marketing_product_id: selected.value as number,
marketing_product: SalesProductToFieldValues(so),
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,
marketing_product_id: undefined,
marketing_product: null,
qty: '',
unit_price: '',
total_price: '',
avg_weight: '',
total_weight: '',
vehicle_number: '',
});
}}
startAdornment={
selectedProduct && (
<Badge
variant='soft'
color='success'
size='sm'
className={{ badge: 'whitespace-nowrap font-semibold' }}
>
{
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label
}
</Badge>
)
return;
}
isClearable
isError={Boolean(formik.errors.marketing_product_id)}
errorMessage={formik.errors.marketing_product_id}
required
/>
<DateInput
name='delivery_date'
label='Tanggal'
value={formik.values.delivery_date ?? undefined}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.delivery_date &&
Boolean(formik.errors.delivery_date)
}
errorMessage={formik.errors.delivery_date}
placeholder='Pilih Tanggal'
className={{
inputWrapper: 'bg-white',
}}
required
/>
<PatternInput
name='vehicle_number'
label='No. Polisi'
format='AA #### AAA'
mask='_'
inputVehicleNumber
required
type='text'
placeholder='B 1234 CDE'
value={formatVechicleNumber(formik.values.vehicle_number ?? '')}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.errors.vehicle_number)}
errorMessage={formik.errors.vehicle_number}
/>
</div>
<div className='divider my-6'></div>
<div className='grid sm:grid-cols-3 gap-4'>
<NumberInput
required
label='Kuantitas'
name='qty'
value={formik.values.qty}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('qty')}
isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-500'>
{isResponseSuccess(productData)
? productData?.data?.uom.name
: ''}
</span>
</div>
}
bottomLabel={
formik.values.marketing_product_id
? 'Stok dijual: ' +
salesOrders?.find(
(item) => item.id === formik.values.marketing_product_id
)?.qty +
' ' +
(isResponseSuccess(productData)
? productData?.data?.uom.name
: '')
: ''
}
/>
<NumberInput
required
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('unit_price')}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
<NumberInput
required
label='Avg. Bobot (Kg)'
name='avg_weight'
value={formik.values.avg_weight}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('avg_weight')}
isError={Boolean(formik.errors.avg_weight)}
errorMessage={formik.errors.avg_weight}
placeholder='Masukan Bobot Rata-rata'
/>
<NumberInput
required
label='Total Bobot (Kg)'
name='total_weight'
value={formik.values.total_weight}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('total_weight')}
isError={Boolean(formik.errors.total_weight)}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
/>
formik.setValues({
...formik.values,
marketing_product_id: selected.value as number,
marketing_product: SalesProductToFieldValues(so),
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,
});
}}
startAdornment={
selectedProduct && (
<StatusBadge
text={
exisitingValues?.find(
(item) => item.id === selectedProduct?.value
)?.marketing_product?.kandang?.label ?? ''
}
color='success'
className={{
badge: 'whitespace-nowrap w-fit font-semibold',
}}
/>
)
}
isClearable
isError={Boolean(formik.errors.marketing_product_id)}
errorMessage={formik.errors.marketing_product_id}
required
/>
<NumberInput
required
label='Total Penjualan (Rp)'
name='total_price'
value={formik.values.total_price}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('total_price')}
isError={Boolean(formik.errors.total_price)}
errorMessage={formik.errors.total_price}
placeholder='Masukan Total Penjualan'
/>
</div>
<PatternInput
name='vehicle_number'
label='No. Polisi'
format='AA #### AAA'
mask='_'
inputVehicleNumber
required
type='text'
placeholder='B 1234 CDE'
value={formatVechicleNumber(formik.values.vehicle_number ?? '')}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.errors.vehicle_number)}
errorMessage={formik.errors.vehicle_number}
/>
<NumberInput
required
label='Kuantitas'
name='qty'
value={formik.values.qty}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('qty')}
isError={Boolean(formik.errors.qty)}
errorMessage={formik.errors.qty}
placeholder='Masukan Kuantitas'
endAdornment={
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-500'>
{isResponseSuccess(productData)
? productData?.data?.uom.name
: ''}
</span>
</div>
}
bottomLabel={
formik.values.marketing_product_id
? 'Stok dijual: ' +
salesOrders?.find(
(item) => item.id === formik.values.marketing_product_id
)?.qty +
' ' +
(isResponseSuccess(productData)
? productData?.data?.uom.name
: '')
: ''
}
/>
<NumberInput
required
label={`Harga / ${isResponseSuccess(productData) ? productData?.data?.uom?.name : 'Produk'} (Rp)`}
name='unit_price'
value={formik.values.unit_price}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('unit_price')}
isError={Boolean(formik.errors.unit_price)}
errorMessage={formik.errors.unit_price}
placeholder='Masukan Harga Satuan'
/>
<NumberInput
required
label='Avg. Bobot (Kg)'
name='avg_weight'
value={formik.values.avg_weight}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('avg_weight')}
isError={Boolean(formik.errors.avg_weight)}
errorMessage={formik.errors.avg_weight}
placeholder='Masukan Bobot Rata-rata'
/>
<NumberInput
required
label='Total Bobot (Kg)'
name='total_weight'
value={formik.values.total_weight}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('total_weight')}
isError={Boolean(formik.errors.total_weight)}
errorMessage={formik.errors.total_weight}
placeholder='Masukan Total Bobot'
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
<NumberInput
required
label='Total Penjualan (Rp)'
name='total_price'
value={formik.values.total_price}
onChange={(e) => {
formik.handleChange(e);
setCurrentInput(e.target.name);
}}
onBlur={() => handleBlurField('total_price')}
isError={Boolean(formik.errors.total_price)}
errorMessage={formik.errors.total_price}
placeholder='Masukan Total Penjualan'
/>
<div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning'>
Reset
</Button>
<AlertErrorList
className={{
alert: 'w-full my-4',
}}
formErrorList={formErrorList}
onClose={close}
/>
<div className='h-18' />
<div className='absolute sm:w-full bottom-0 right-0'>
<Button
type='submit'
isLoading={formik.isSubmitting}
disabled={formik.isSubmitting}
className='w-full p-3 rounded-lg text-base-100 text-sm font-semibold'
>
Submit
</Button>
@@ -6,27 +6,20 @@ import {
SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { RefObject, useMemo, useState } from 'react';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang';
import { ProductApi, UomApi, WarehouseApi } from '@/services/api/master-data';
import { WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper';
import {
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { formatNumber, formatVechicleNumber } from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import useSWR from 'swr';
import SelectInputRadio from '@/components/input/SelectInputRadio';
const roundWeight = (value: number) => Number(value.toFixed(2));
const roundPrice = (value: number) => Math.round(value);
@@ -282,7 +275,7 @@ const SalesOrderProductForm = ({
return (
<>
<form
className='size-full flex flex-col p-4 relative overflow-x-hidden'
className='size-full flex flex-col gap-1.5 relative'
onSubmit={handleFormSubmit}
onReset={handleResetForm}
>
@@ -311,7 +304,7 @@ const SalesOrderProductForm = ({
}
errorMessage={formik.errors.vehicle_number}
/>
<SelectInput
<SelectInputRadio
required
label='Kandang'
options={kandangSourceOptions}
@@ -327,7 +320,7 @@ const SalesOrderProductForm = ({
errorMessage={formik.errors.kandang_id}
placeholder='Pilih Kandang'
/>
<SelectInput
<SelectInputRadio
required
label='Produk'
options={productOptionsFiltered}
@@ -453,12 +446,12 @@ const SalesOrderProductForm = ({
<div className='h-18' />
<div className='absolute p-4 sm:w-[446px] bottom-0 right-0'>
<div className='absolute w-full bottom-0 right-0'>
<Button
type='submit'
isLoading={formik.isSubmitting}
disabled={formik.isSubmitting}
className='w-full rounded-lg'
className='w-full p-3 rounded-lg text-base-100'
>
Submit
</Button>
@@ -1,21 +1,27 @@
import Table from '@/components/Table';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import * as TanStack from '@tanstack/react-table';
import { useMemo, useRef } from 'react';
import {
cn,
formatCurrency,
formatDate,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { useRef } from 'react';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import { Marketing } from '@/types/api/marketing/marketing';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
onEdit: (id: number) => void;
formType?:
| 'add'
| 'edit'
| 'add_deliver'
| 'edit_deliver'
| 'add_delivery'
| 'edit_delivery'
| 'detail'
| 'rejected'
| 'pending'
| string
| null;
marketing?: Marketing;
onEdit: (id: number, values: DeliveryOrderProductFormValues) => void;
onDelete: (id: number) => void;
onAddProductClick: () => void;
};
@@ -26,201 +32,168 @@ const DeliveryOrderProductTable = ({
onEdit,
onDelete,
onAddProductClick,
marketing,
}: DeliveryOrderProductTableProps) => {
const onEditRef = useRef(onEdit);
onEditRef.current = onEdit;
const onDeleteRef = useRef(onDelete);
onDeleteRef.current = onDelete;
const canAddData = data.filter((item) => !Boolean(item.qty));
const columns = useMemo(() => {
const cols = [
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
header: 'No. Pengiriman',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<>
{props.row.original.do_number ? props.row.original.do_number : '-'}
</>
),
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.vehicle_number,
header: 'No. Polisi',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.vehicle_number
? formatVechicleNumber(props.row.original.vehicle_number as string)
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.marketing_product?.kandang?.label,
header: 'Kandang',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.marketing_product?.product_warehouse?.label,
header: 'Produk',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
row.delivery_date
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
: '-',
header: 'Tanggal Delivery',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.delivery_date
? formatDate(
props.row.original.delivery_date as string,
'DD MMM YYYY'
)
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.unit_price,
header: 'Harga Satuan (Rp)',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.unit_price
? formatCurrency(
parseFloat(props.row.original.unit_price as string)
)
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.total_weight,
header: 'Total Bobot (Kg)',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.total_weight
? formatNumber(
parseFloat(props.row.original.total_weight as string)
)
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.qty,
header: 'Kuantitas',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.qty
? formatNumber(parseFloat(props.row.original.qty as string))
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.avg_weight,
header: 'Avg. Bobot (Kg)',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.avg_weight
? formatNumber(parseFloat(props.row.original.avg_weight as string))
: '-',
},
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.total_price,
header: 'Total Penjualan (Rp)',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) =>
props.row.original.total_price
? formatCurrency(
parseFloat(props.row.original.total_price as string)
)
: '-',
},
{
header: 'Aksi',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<>
{props.row.original.qty && (
<>
<Button
color='warning'
className='px-2 py-1 text-sm'
onClick={() =>
onEditRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:edit' width={16} height={16} /> Edit
</Button>
<Button
color='error'
className='px-2 py-1 text-sm'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
disabled={!!props.row.original.do_number}
>
<Icon icon='mdi:delete' width={16} height={16} /> Hapus
</Button>
</>
)}
{!props.row.original.qty && '-'}
</>
</div>
),
},
];
if (formType == 'add_deliver') {
return cols.filter((col) => col.header != 'No. Pengiriman');
}
return cols;
}, [formType, onEditRef]);
return (
<>
<Table<DeliveryOrderProductFormValues>
data={data}
columns={columns}
className={{
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-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-2 py-2 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
emptyContent={
<div
className={cn(
'w-full h-16 flex flex-col justify-center items-center gap-2'
)}
>
<span className='text-gray-500'>Belum ada data pengiriman</span>
</div>
}
/>
<div className='flex flex-row gap-3 mt-3'>
<Button
type='button'
variant='outline'
className='justify-start w-fit py-1 text-sm'
onClick={onAddProductClick}
disabled={!canAddData}
>
<Icon icon='mdi:plus' width={16} height={16} />
Tambah Pengiriman
</Button>
<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',
}}
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>
);
})}
</div>
</>
);
@@ -1,16 +1,14 @@
'use client';
import Button from '@/components/Button';
import Table from '@/components/Table';
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import {
cn,
formatCurrency,
formatNumber,
formatVechicleNumber,
} from '@/lib/helper';
import { Icon } from '@iconify/react';
import { useMemo, useRef, useState } from 'react';
import { useMemo, useRef } from 'react';
import * as TanStack from '@tanstack/react-table';
import CheckboxInput from '@/components/input/CheckboxInput';
@@ -156,15 +154,51 @@ const SalesOrderProductTable = ({
style={{
borderRadius: '0.5rem',
}}
className='border-none'
className='border-none w-full'
>
<tbody>
<tbody className='w-full'>
<tr className='border-b border-tools-table-outline border-base-content/5'>
<th className='text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
<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'>
Value
<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>
<>
@@ -178,48 +212,47 @@ const SalesOrderProductTable = ({
{item.product_warehouse?.label}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Kategori</td>
<td className='text-sm px-4 py-3'>Telur</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>
<tr>
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
<td className='text-sm px-4 py-3'>Peti 15Kg</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Peti</td>
<td className='text-sm px-4 py-3'>
{formatNumber(parseFloat(item.qty as string))}
</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))}
{item.total_weight
? formatNumber(
parseFloat(item.total_weight as string)
) + ' Kg'
: '-'}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>Total Butir Telur</td>
<td className='text-sm px-4 py-3'>Avg Bobot</td>
<td className='text-sm px-4 py-3'>
{formatNumber(parseFloat(item.qty as string))}
{item.avg_weight
? formatNumber(parseFloat(item.avg_weight as string)) +
' Kg'
: '-'}
</td>
</tr>
<tr>
<td className='text-sm px-4 py-3'>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'>Total Harga Satuan</td>
<td className='text-sm px-4 py-3'>
{formatNumber(parseFloat(item.unit_price as string))}
{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'>
{formatNumber(parseFloat(item.total_price as string))}
{formatCurrency(parseFloat(item.total_price as string))}
</td>
</tr>
</>
@@ -233,7 +266,7 @@ const SalesOrderProductTable = ({
<Button
type='button'
variant='outline'
className='justify-center w-full rounded-lg text-center text-sm mt-5'
className='justify-center p-3 w-full rounded-lg text-center text-sm mt-3'
onClick={onAddProductClick}
>
Tambah Produk