feat(FE-181-179-220-271): adding SO export PDF and adjusting delivery form

This commit is contained in:
randy-ar
2025-11-20 18:15:42 +07:00
parent b33e7a1919
commit 391b355e8d
26 changed files with 1490 additions and 245 deletions
@@ -1,16 +1,17 @@
'use client';
import SalesForm from '@/components/pages/marketing/form/MarketingForm';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('salesOrderId');
const soId = searchParams.get('marketingId');
const {
data: marketing,
@@ -34,12 +35,13 @@ const EditMarketingDelivery = () => {
router.replace('/404');
return;
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<SalesForm
formType='deliver'
<MarketingForm
formType='add_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
@@ -1,9 +1,9 @@
import SalesForm from '@/components/pages/marketing/form/MarketingForm';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
const AddSalesOrder = () => {
return (
<div className='size-full p-4'>
<SalesForm />
<MarketingForm formType='add' />
</div>
);
};
@@ -0,0 +1,62 @@
'use client';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import toast from 'react-hot-toast';
import useSWR from 'swr';
const EditMarketingDelivery = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('marketingId');
const {
data: marketing,
isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(`get-so-${soId}`, () =>
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
);
if (!soId) {
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 (!isLoading && (!marketing || isResponseError(marketing))) {
router.replace('/404');
return;
}
if (
isResponseSuccess(marketing) &&
marketing.data.latest_approval.step_number != 3
) {
toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
router.back();
}
return (
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<MarketingForm
formType='edit_deliver'
initialValues={marketing.data}
afterSubmit={() => {
refreshMarketing();
}}
/>
)}
</div>
);
};
export default EditMarketingDelivery;
+11
View File
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,16 +1,16 @@
'use client';
import SalesOrderDetail from '@/components/pages/marketing/detail/MarketingDetail';
import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const DetailSalesOrder = () => {
const DetailMarketing = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('salesOrderId');
const soId = searchParams.get('marketingId');
const {
data: marketing,
@@ -37,7 +37,7 @@ const DetailSalesOrder = () => {
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<SalesOrderDetail
<MarketingDetail
initialValues={marketing.data}
refresh={refreshMarketing}
/>
@@ -46,4 +46,4 @@ const DetailSalesOrder = () => {
);
};
export default DetailSalesOrder;
export default DetailMarketing;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -1,6 +1,6 @@
'use client';
import SalesForm from '@/components/pages/marketing/form/MarketingForm';
import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -10,7 +10,7 @@ const EditSalesOrder = () => {
const router = useRouter();
const searchParams = useSearchParams();
const soId = searchParams.get('salesOrderId');
const soId = searchParams.get('marketingId');
const {
data: marketing,
@@ -38,7 +38,7 @@ const EditSalesOrder = () => {
<div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && (
<SalesForm
<MarketingForm
formType='edit'
initialValues={marketing.data}
afterSubmit={() => {
+10
View File
@@ -0,0 +1,10 @@
import MarketingTable from '@/components/pages/marketing/MarketingTable';
const Marketing = () => {
return (
<div className='w-full p-4'>
<MarketingTable />
</div>
);
};
export default Marketing;
-10
View File
@@ -1,10 +0,0 @@
import SalesOrderTable from '@/components/pages/marketing/MarketingTable';
const SalesOrder = () => {
return (
<div className='w-full p-4'>
<SalesOrderTable />
</div>
);
};
export default SalesOrder;
@@ -22,6 +22,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { CellContext, Row } from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import { useCallback, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
@@ -30,10 +31,12 @@ const RowsOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
deliveryClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Marketing, unknown>;
deleteClickHandler: () => void;
deliveryClickHandler?: () => void;
}) => {
return (
<div
@@ -48,7 +51,7 @@ const RowsOptionsMenu = ({
>
<div className='flex flex-col gap-1'>
<Button
href={`/marketing/sales-orders/detail/?salesOrderId=${props.row.original.id}`}
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
@@ -58,7 +61,18 @@ const RowsOptionsMenu = ({
</Button>
{props.row.original.latest_approval.step_number != 1 && (
<Button
href={`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${props.row.original.id}`}
href={
props.row.original.latest_approval.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
: props.row.original.latest_approval.step_number == 2
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
: undefined
}
onClick={() => {
if (props.row.original.latest_approval.step_number == 2) {
deliveryClickHandler?.();
}
}}
variant='ghost'
color='success'
className='justify-start text-sm'
@@ -69,7 +83,7 @@ const RowsOptionsMenu = ({
)}
{props.row.original.latest_approval.step_number != 3 && (
<Button
href={`/marketing/sales-orders/detail/edit?salesOrderId=${props.row.original.id}`}
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
@@ -92,7 +106,7 @@ const RowsOptionsMenu = ({
);
};
const SalesOrderTable = () => {
const MarketingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
@@ -103,6 +117,8 @@ const SalesOrderTable = () => {
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const router = useRouter();
const {
data: marketing,
isLoading: isLoadingMarketing,
@@ -112,6 +128,7 @@ const SalesOrderTable = () => {
const deleteModal = useModal();
const confirmationModal = useModal();
const productsModal = useModal();
const deliveryModal = useModal();
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -221,6 +238,16 @@ const SalesOrderTable = () => {
refreshMarketing();
};
const confirmationModalDeliveryClickHandler = async (notes: string) => {
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
deliveryModal.closeModal();
toast.success(res?.message as string);
refreshMarketing?.();
router.push(
`/marketing/detail/delivery-orders/edit?marketingId=${selectedItem?.id}`
);
};
const {
state: tableFilterState,
updateFilter,
@@ -246,7 +273,7 @@ const SalesOrderTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: '/marketing/sales-orders/add',
href: '/marketing/add/sales-orders',
label: 'Tambah Sales Order',
}}
search={{
@@ -411,6 +438,11 @@ const SalesOrderTable = () => {
deleteModal.openModal();
};
const deliveryClickHandler = () => {
setSelectedItem(props.row.original);
deliveryModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
@@ -419,6 +451,7 @@ const SalesOrderTable = () => {
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
deliveryClickHandler={deliveryClickHandler}
/>
</RowDropdownOptions>
)}
@@ -429,6 +462,7 @@ const SalesOrderTable = () => {
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
deliveryClickHandler={deliveryClickHandler}
/>
</RowCollapseOptions>
)}
@@ -493,6 +527,19 @@ const SalesOrderTable = () => {
onClick: deleteMarketingHandler,
}}
/>
<ConfirmationModalWithNotes
ref={deliveryModal.ref}
type={'success'}
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
onClick: confirmationModalDeliveryClickHandler,
}}
/>
<Modal
ref={productsModal.ref}
@@ -554,4 +601,4 @@ const SalesOrderTable = () => {
</>
);
};
export default SalesOrderTable;
export default MarketingTable;
@@ -24,7 +24,6 @@ import {
} from '@/services/api/marketing/marketing';
import {
BaseDelivery,
BaseDeliveryOrder,
BaseSalesOrder,
Marketing,
} from '@/types/api/marketing/marketing';
@@ -32,8 +31,9 @@ import { Icon } from '@iconify/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import toast from 'react-hot-toast';
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
const SalesOrderDetail = ({
const MarketingDetail = ({
initialValues,
refresh,
}: {
@@ -118,17 +118,14 @@ const SalesOrderDetail = ({
refresh?.();
refreshApproval?.();
router.push(
`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${initialValues?.id}`
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
);
};
return (
<>
<div className='flex flex-col w-full gap-4'>
<FormHeader
title='Detail Sales Order'
backUrl='/marketing/sales-orders'
/>
<FormHeader title='Detail Sales Order' backUrl='/marketing' />
{!isLoadingApproval && approvals && (
<ApprovalSteps approvals={approvals} />
)}
@@ -156,8 +153,7 @@ const SalesOrderDetail = ({
{initialValues?.latest_approval?.step_number == 2 && (
<Button
color='success'
// href={`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${initialValues?.id}`}
onClick={deliveryClickHandler}
href={`/marketing/add/delivery-orders?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:truck' width={24} height={24} />
Delivery Order
@@ -210,10 +206,7 @@ const SalesOrderDetail = ({
<td className='font-semibold'>Dokumen</td>
<td>:</td>
<td>
<Button className='py-2 px-3 font-medium text-md'>
<Icon icon='mdi:file-pdf' width={16} height={16} />
{initialValues?.so_number}
</Button>
<SalesOrderExport data={initialValues} />
</td>
</tr>
</tbody>
@@ -407,7 +400,7 @@ const SalesOrderDetail = ({
<Button
color='warning'
type='button'
href={`/marketing/sales-orders/detail/edit?salesOrderId=${initialValues?.id}`}
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
@@ -464,4 +457,4 @@ const SalesOrderDetail = ({
);
};
export default SalesOrderDetail;
export default MarketingDetail;
@@ -51,7 +51,22 @@ export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
delivery_order: Yup.array()
.of(DeliveryOrderProductSchema)
.min(1, 'Pengiriman wajib diisi!')
.required(),
.required()
.test(
'at-least-one-delivery-date',
'Minimal ada satu tanggal pengiriman yang harus diisi!',
(value) => {
if (!value || value.length == 0) {
return false;
}
return value.some(
(item) =>
item.delivery_date !== null &&
item.delivery_date !== undefined &&
item.delivery_date !== ''
);
}
),
});
export const UpdateSalesOrderSchema = SalesOrderSchema;
@@ -14,13 +14,15 @@ import { formatCurrency, formatDate } from '@/lib/helper';
import {
BaseDeliveryOrder,
BaseSalesOrder,
CreateDeliveryOrderPayload,
CreateSalesOrderPayload,
CreateSalesOrderProductPayload,
Marketing,
UpdateDeliveryOrderPayload,
UpdateSalesOrderPayload,
} from '@/types/api/marketing/marketing';
import { Icon } from '@iconify/react';
import { useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Customer } from '@/types/api/master-data/customer';
import { CustomerApi } from '@/services/api/master-data';
import { useFormik } from 'formik';
@@ -46,6 +48,11 @@ import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
const MarketingProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
@@ -113,12 +120,37 @@ const DeliveryProductToFieldValues = (
return data;
};
const SalesForm = ({
const mergeSOwithDO = (
salesOrders: SalesOrderProductFormValues[],
deliveryOrders: DeliveryOrderProductFormValues[]
): 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: delivery?.unit_price ?? so.unit_price,
total_weight: delivery?.total_weight ?? so.total_weight,
qty: delivery?.qty ?? so.qty,
avg_weight: delivery?.avg_weight ?? so.avg_weight,
total_price: delivery?.total_price ?? so.total_price,
marketing_product: so, // jika ada, override
} as DeliveryOrderProductFormValues;
});
};
const MarketingForm = ({
formType = 'add',
initialValues,
afterSubmit,
}: {
formType?: 'add' | 'edit' | 'deliver';
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
initialValues?: Marketing;
afterSubmit?: () => void;
}) => {
@@ -131,6 +163,17 @@ const SalesForm = ({
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
useState<DeliveryOrderProductFormValues | null>(null);
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[]
>(
mergeSOwithDO(
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
initialValues?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
) ?? []
)
);
// Repeater Props
const addSOModal = useModal();
const addDOModal = useModal();
@@ -171,10 +214,12 @@ const SalesForm = ({
initialValues?.sales_order?.map((product) =>
MarketingProductToFieldValues(product)
) ?? [],
delivery_order:
delivery_order: mergeSOwithDO(
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
initialValues?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
) ?? [],
) ?? []
),
};
}, [initialValues]);
@@ -182,11 +227,13 @@ const SalesForm = ({
enableReinitialize: true,
initialValues: formikInitialValues,
validationSchema:
formType === 'deliver' ? DeliveryOrderSchema : SalesOrderSchema,
formType == 'add_deliver' || formType == 'edit_deliver'
? DeliveryOrderSchema
: SalesOrderSchema,
validateOnMount: true,
onSubmit: async (values) => {
const payload =
formType != 'deliver'
formType != 'add_deliver' && formType != 'edit_deliver'
? ({
customer_id: values.customer_id as number,
sales_person_id: values.sales_person_id as number,
@@ -207,21 +254,26 @@ const SalesForm = ({
} as CreateSalesOrderPayload)
: ({
marketing_id: initialValues?.id as number,
delivery_products: values.delivery_order.map((product) => {
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,
};
}),
delivery_products: values.delivery_order
.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);
console.log('PAYLOAD');
console.log(payload);
@@ -230,9 +282,12 @@ const SalesForm = ({
await createMarketingHandler(payload as CreateSalesOrderPayload);
break;
case 'edit':
await updateMarketingHandler(payload as CreateSalesOrderPayload);
await updateMarketingHandler(payload as UpdateSalesOrderPayload);
break;
case 'deliver':
case 'add_deliver':
await createDeliveryHandler(payload as CreateDeliveryOrderPayload);
break;
case 'edit_deliver':
await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
break;
default:
@@ -256,15 +311,14 @@ const SalesForm = ({
const createMarketingRes = await SalesOrderApi.create(values);
if (isResponseSuccess(createMarketingRes)) {
toast.success(createMarketingRes?.message as string);
router.push('/marketing/sales-orders');
router.push('/marketing');
}
if (isResponseError(createMarketingRes)) {
toast.error(createMarketingRes?.message as string);
}
afterSubmit?.();
setIsLoading(false);
};
const updateMarketingHandler = async (values: CreateSalesOrderPayload) => {
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
setIsLoading(true);
console.log(values);
const updateMarketingRes = await SalesOrderApi.update(
@@ -273,29 +327,52 @@ const SalesForm = ({
);
if (isResponseSuccess(updateMarketingRes)) {
toast.success(updateMarketingRes?.message as string);
router.push(
`/marketing/sales-orders/detail?salesOrderId=${initialValues?.id}`
);
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
}
if (isResponseError(updateMarketingRes)) {
toast.error(updateMarketingRes?.message as string);
}
afterSubmit?.();
setIsLoading(false);
};
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const createDeliveryRes = await DeliveryOrderApi.create(values);
if (isResponseSuccess(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.success(createDeliveryRes?.message as string);
setDeliveryOrderValues(
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(
createDeliveryRes.data?.sales_order,
delivery
)
) ?? []
);
router.push(
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
);
}
if (isResponseError(createDeliveryRes)) {
console.log(createDeliveryRes);
toast.error(createDeliveryRes?.message as string);
}
setIsLoading(false);
};
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
setIsLoading(true);
console.log(initialValues?.id);
const updateDeliveryRes =
initialValues?.delivery_order && initialValues?.delivery_order.length > 0
? await DeliveryOrderApi.update(initialValues?.id as number, values)
: await DeliveryOrderApi.update(initialValues?.id as number, values);
const updateDeliveryRes = await DeliveryOrderApi.update(
initialValues?.id as number,
values
);
if (isResponseSuccess(updateDeliveryRes)) {
console.log(updateDeliveryRes);
toast.success(updateDeliveryRes?.message as string);
formik.setFieldValue(
'delivery_order',
// router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
setDeliveryOrderValues(
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
DeliveryProductToFieldValues(
updateDeliveryRes.data?.sales_order,
@@ -308,7 +385,6 @@ const SalesForm = ({
console.log(updateDeliveryRes);
toast.error(updateDeliveryRes?.message as string);
}
afterSubmit?.();
setIsLoading(false);
};
@@ -360,9 +436,9 @@ const SalesForm = ({
);
setRowSOSelection({});
}, [formik, selectedRowSOIds]);
const handleDelete = () => {
const handleDelete = useCallback(() => {
deleteModal.openModal();
};
}, [deleteModal]);
const handleAddSOClick = useCallback(() => {
setSelectedMarketingProduct(null);
addSOModal.openModal();
@@ -385,10 +461,7 @@ const SalesForm = ({
const handleDeleteDO = useCallback(
(id: number) => {
const currentProducts = formik.values.delivery_order;
formik.setFieldValue(
'delivery_order',
currentProducts.filter((p) => p.id != id)
);
setDeliveryOrderValues((prev) => prev.filter((p) => p.id !== id));
},
[formik]
);
@@ -403,46 +476,50 @@ const SalesForm = ({
[formik]
);
const handleBulkDeleteDO = useCallback(() => {
const currentProducts = formik.values.delivery_order;
formik.setFieldValue(
'delivery_order',
currentProducts.filter(
(product) => !selectedRowDOIds.includes(product.id ?? -1)
)
setDeliveryOrderValues((prev) =>
prev.filter((product) => !selectedRowDOIds.includes(product.id ?? -1))
);
setRowDOSelection({});
}, [formik, selectedRowDOIds]);
const handleAddDOClick = useCallback(() => {
setSelectedDeliveryProduct(null);
addDOModal.openModal();
}, [addDOModal]);
const handleAddSubmitDO = useCallback(
async (values: DeliveryOrderProductFormValues) => {
const currentProducts = formik.values.delivery_order;
const newValues = {
...values,
id: values.id ?? Date.now(),
};
formik.setFieldValue('delivery_order', [...currentProducts, newValues]);
setDeliveryOrderValues((prev) => [...prev, newValues]);
addDOModal.closeModal();
},
[formik, addDOModal]
);
const handleInputDate = useCallback(
(newData: DeliveryOrderProductFormValues) => {
setDeliveryOrderValues((prev) => {
return prev.map((item) => {
if (item.marketing_product_id == newData.marketing_product_id) {
return newData;
}
return item;
});
});
},
[]
);
const handleUpdateDO = useCallback(
async (id: number, values: DeliveryOrderProductFormValues) => {
formik.setFieldValue(
'delivery_order',
formik.values.delivery_order.map((product) => {
if (product.id === id) {
return {
...product,
...values,
};
}
return product;
})
setDeliveryOrderValues((prev) =>
prev.map((product) =>
product.id === id ? { ...product, ...values } : product
)
);
setSelectedDeliveryProduct(null);
addDOModal.closeModal();
@@ -453,7 +530,9 @@ const SalesForm = ({
const memoSalesOrder = formik.values.sales_order;
const memoDeliveryOrder = formik.values.delivery_order;
useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues);
}, [deliveryOrderValues, initialValues]);
return (
<>
@@ -463,8 +542,8 @@ const SalesForm = ({
onReset={formik.handleReset}
>
<FormHeader
title={`${formType === 'add' ? 'Tambah' : 'Edit'} Sales Order`}
backUrl='/marketing/sales-orders'
title={`${formType == 'add' || formType == 'add_deliver' ? 'Tambah' : 'Edit'} ${formType === 'add_deliver' || formType === 'edit_deliver' ? 'Delivery' : 'Sales'} Order`}
backUrl='/marketing'
/>
<Card
title='Informasi Order'
@@ -485,7 +564,9 @@ const SalesForm = ({
errorMessage={formik.errors.customer_id}
isClearable
placeholder='Pilih Pelanggan'
isDisabled={formType === 'deliver'}
isDisabled={
formType === 'add_deliver' || formType === 'edit_deliver'
}
/>
<DateInput
name='so_date'
@@ -495,31 +576,33 @@ const SalesForm = ({
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
errorMessage={formik.errors.so_date}
placeholder='Pilih Tanggal'
readOnly={formType == 'deliver'}
readOnly={formType == 'add_deliver' || formType == 'edit_deliver'}
/>
</div>
</Card>
<Card
title='Informasi Produk'
className={{
wrapper: 'bg-white w-full',
}}
>
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
{(formType == 'add' || formType == 'edit') && (
<Card
title='Informasi Produk'
className={{
wrapper: 'bg-white w-full',
}}
>
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
<div className='text-green-500'>{JSON.stringify(formik.values)}</div>
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
<SalesOrderProductTable
formType={formType}
data={memoSalesOrder}
rowSelection={rowSOSelection}
setRowSelection={setRowSOSelection}
selectedRowIds={selectedRowSOIds}
onDelete={handleDeleteSO}
onBulkDelete={handleBulkDeleteSO}
onAddProductClick={handleAddSOClick}
/>
</Card>
{formType == 'deliver' &&
<MemoizedSalesOrderProductTable
formType={formType}
data={memoSalesOrder}
rowSelection={rowSOSelection}
setRowSelection={setRowSOSelection}
selectedRowIds={selectedRowSOIds}
onDelete={handleDeleteSO}
onBulkDelete={handleBulkDeleteSO}
onAddProductClick={handleAddSOClick}
/>
</Card>
)}
{(formType == 'add_deliver' || formType == 'edit_deliver') &&
initialValues?.sales_order &&
initialValues?.sales_order.length > 0 && (
<Card
@@ -528,11 +611,14 @@ const SalesForm = ({
wrapper: 'bg-white w-full',
}}
>
{/* {JSON.stringify(memoSalesOrder)}
{JSON.stringify(memoDeliveryOrder)} */}
<DeliveryOrderProductTable
{/* {JSON.stringify(memoSalesOrder)} */}
{/* <small>{JSON.stringify(memoDeliveryOrder)}</small> */}
{/* <small className='block text-error'>
{JSON.stringify(formik.errors)}
</small> */}
<MemoizedDeliveryOrderProductTable
formType={formType}
data={memoDeliveryOrder}
data={deliveryOrderValues}
salesOrder={memoSalesOrder}
rowSelection={rowDOSelection}
setRowSelection={setRowDOSelection}
@@ -541,6 +627,7 @@ const SalesForm = ({
onEdit={handleEditDO}
onBulkDelete={handleBulkDeleteDO}
onAddProductClick={handleAddDOClick}
onInputDate={handleInputDate}
/>
</Card>
)}
@@ -556,7 +643,7 @@ const SalesForm = ({
onChange={formik.handleChange}
isError={formik.touched.notes && Boolean(formik.errors.notes)}
errorMessage={formik.errors.notes}
disabled={formType === 'deliver'}
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
/>
<div className='flex flex-col h-full justify-between items-end py-6'>
<span>Total Penjualan</span>
@@ -611,7 +698,7 @@ const SalesForm = ({
</Button>
</div>
<div>
<SalesOrderProductForm
<MemoizedSalesOrderProductForm
onSubmitForm={handleAddSubmitSO}
initialValues={selectedMarketingProduct ?? undefined}
/>
@@ -640,7 +727,7 @@ const SalesForm = ({
</Button>
</div>
<div>
<DeliveryOrderProductForm
<MemoizedDeliveryOrderProductForm
salesOrders={initialValues?.sales_order ?? []}
onSubmitForm={handleAddSubmitDO}
initialValues={selectedDeliveryProduct ?? undefined}
@@ -667,4 +754,4 @@ const SalesForm = ({
);
};
export default SalesForm;
export default MarketingForm;
@@ -15,7 +15,7 @@ type DeliveryOrderProductSchemaType = {
avg_weight: string | number | undefined;
total_price: string | number | undefined;
vehicle_number: string | undefined;
delivery_date: string | undefined;
delivery_date: string | undefined | null;
do_number?: string | undefined | null; // Uncertain
};
@@ -30,7 +30,7 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
.min(1, 'Harga Satuan wajib diisi!')
.required('Harga Satuan wajib diisi!'),
total_weight: Yup.number()
.min(1, 'Total Bobot wajib diisi!')
.min(0, 'Total Bobot wajib diisi!')
.required('Total Bobot wajib diisi!'),
qty: Yup.number()
.min(1, 'Kuantitas wajib diisi!')
@@ -42,7 +42,10 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
.min(1, 'Total Penjualan wajib diisi!')
.required('Total Penjualan wajib diisi!'),
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
delivery_date: Yup.string().required('Tanggal Pengiriman wajib diisi!'),
delivery_date: Yup.string()
.required('Tanggal Pengiriman wajib diisi!')
.nullable()
.optional(),
do_number: Yup.string().nullable().optional(),
});
@@ -49,8 +49,8 @@ const DeliveryOrderProductForm = ({
marketing_product: initialValues?.marketing_product || undefined,
},
validationSchema: DeliveryOrderProductSchema,
validateOnBlur: false,
validateOnChange: true,
validateOnBlur: true,
validateOnChange: false,
onSubmit: async (values) => {
setFormErrorMessage('');
if (initialValues?.id) {
@@ -172,42 +172,21 @@ const DeliveryOrderProductForm = ({
)}
<div className='grid grid-cols-2 gap-4'>
<DateInput
name='delivery_date'
label='Tanggal'
value={formik.values.delivery_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.delivery_date &&
Boolean(formik.errors.delivery_date)
}
errorMessage={formik.errors.delivery_date}
placeholder='Pilih Tanggal'
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}
/>
<SelectInput
options={options}
label='Produk'
placeholder='Pilih Produk'
value={selectedProduct}
isDisabled
value={
selectedProduct
? ({
value: selectedProduct?.value,
label: salesOrders.find(
(item) => item.id === selectedProduct?.value
)?.product_warehouse.product.name,
} as OptionType)
: null
}
onChange={(value) => {
const selected = value as OptionType;
setSelectedProduct(selected);
@@ -263,6 +242,36 @@ const DeliveryOrderProductForm = ({
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'
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}
/>
<NumberInput
required
@@ -42,7 +42,7 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
.min(1, 'Harga Satuan wajib diisi!')
.required('Harga Satuan wajib diisi!'),
total_weight: Yup.number()
.min(1, 'Total Bobot wajib diisi!')
.min(0, 'Total Bobot wajib diisi!')
.required('Total Bobot wajib diisi!'),
qty: Yup.number()
.min(1, 'Kuantitas wajib diisi!')
@@ -161,6 +161,9 @@ const SalesOrderProductForm = ({
</Alert>
</div>
)}
{/* <small className='block text-rose-500'>
{JSON.stringify(formik.errors)}
</small> */}
<div className='grid grid-cols-2 gap-4 z-200'>
<PatternInput
name='vehicle_number'
@@ -13,11 +13,12 @@ import {
formatVechicleNumber,
} from '@/lib/helper';
import { SalesOrderProductFormValues } from '../repeater/sales-order/SalesOrderProduct.schema';
import DateInput from '@/components/input/DateInput';
type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
salesOrder: SalesOrderProductFormValues[];
formType?: 'add' | 'edit' | 'deliver';
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
rowSelection: Record<string, boolean>;
setRowSelection: React.Dispatch<
React.SetStateAction<Record<string, boolean>>
@@ -27,6 +28,7 @@ type DeliveryOrderProductTableProps = {
onEdit: (id: number) => void;
onBulkDelete: () => void;
onAddProductClick: () => void;
onInputDate: (data: DeliveryOrderProductFormValues) => void;
};
const DeliveryOrderProductTable = ({
@@ -40,6 +42,7 @@ const DeliveryOrderProductTable = ({
onEdit,
onBulkDelete,
onAddProductClick,
onInputDate,
}: DeliveryOrderProductTableProps) => {
const onDeleteRef = useRef(onDelete);
const onEditRef = useRef(onDelete);
@@ -53,51 +56,86 @@ const DeliveryOrderProductTable = ({
return acc && deliveredQty.length != salesOrder.length;
}, true);
const columns = useMemo(
() => [
{
id: 'select',
header: ({
table,
}: {
table: TanStack.Table<DeliveryOrderProductFormValues>;
}) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({
row,
}: {
row: TanStack.Row<DeliveryOrderProductFormValues>;
}) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
},
const columns = useMemo(() => {
const cols = [
// {
// id: 'select',
// header: ({
// table,
// }: {
// table: TanStack.Table<DeliveryOrderProductFormValues>;
// }) => (
// <div className='w-full flex flex-row justify-center'>
// <CheckboxInput
// name='allRow'
// checked={table.getIsAllRowsSelected()}
// indeterminate={table.getIsSomeRowsSelected()}
// onChange={table.getToggleAllRowsSelectedHandler()}
// />
// </div>
// ),
// cell: ({
// row,
// }: {
// row: TanStack.Row<DeliveryOrderProductFormValues>;
// }) => (
// <div>
// <CheckboxInput
// name='row'
// checked={row.getIsSelected()}
// disabled={!row.getCanSelect()}
// indeterminate={row.getIsSomeSelected()}
// onChange={row.getToggleSelectedHandler()}
// />
// </div>
// ),
// },
{
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
header: 'No. Pengiriman',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => props.row.original.do_number ?? '-',
) => <div>{props.row.original.do_number}</div>,
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
formatDate(row.delivery_date as string, 'DD MMM YYYY'),
row.delivery_date
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
: '-',
header: 'Tanggal Delivery',
cell: (
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<>
{formType == 'add_deliver' && (
<DateInput
name={`delivery_date_${props.row.original.marketing_product_id}`}
className={{
input: 'p-0',
inputWrapper: 'py-1 px-3 h-fit w-fit bg-white',
wrapper: 'p-0',
}}
value={
props.row.original.delivery_date
? formatDate(props.row.original.delivery_date, 'yyyy-MM-DD')
: undefined
}
onChange={(val) => {
onInputDate({
...props.row.original,
delivery_date: val.target.value,
});
}}
/>
)}
{formType == 'edit_deliver' &&
formatDate(
props.row.original.delivery_date as string,
'DD MMM YYYY'
)}
</>
),
},
{
accessorFn: (row: DeliveryOrderProductFormValues) =>
@@ -145,30 +183,39 @@ const DeliveryOrderProductTable = ({
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<Button
color='success'
className='p-1'
onClick={() => onEditRef.current(props.row.original.id as number)}
type='button'
>
<Icon icon='mdi:edit' width={16} height={16} />
</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} />
</Button>
<>
<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='p-1'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:trash' width={16} height={16} />
</Button> */}
</>
</div>
),
},
],
[]
);
];
if (formType == 'add_deliver') {
return cols.filter(
(col) => col.header != 'Aksi' && col.header != 'No. Pengiriman'
);
}
return cols;
}, [formType, onInputDate, onEditRef]);
return (
<>
@@ -185,7 +232,7 @@ const DeliveryOrderProductTable = ({
'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 first:flex first:flex-row first:justify-start',
'px-2 py-2 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
emptyContent={
@@ -199,16 +246,16 @@ const DeliveryOrderProductTable = ({
}
/>
<div className='flex flex-row gap-3 mt-3'>
<Button
{/* <Button
type='button'
variant='outline'
className='justify-start w-fit py-1 text-sm'
onClick={onAddProductClick}
disabled={!canAddData}
// disabled={!canAddData}
>
<Icon icon='mdi:plus' width={16} height={16} />
Tambah Pengiriman
</Button>
</Button> */}
{selectedRowIds.length > 0 && (
<Button
type='button'
@@ -0,0 +1,227 @@
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 { useMemo, useState } from 'react';
import pdfStyles from './styles/MarketingPDFStyles';
import { formatDate, formatNumber } from '@/lib/helper';
interface SalesOrderExportProps {
data?: Marketing;
className?: string;
}
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
const salesData = data;
const handleDownloadPDF = async () => {
if (!salesData) {
alert('No sales order data available');
return;
}
setIsGeneratingPDF(true);
try {
const blob = await pdf(<PDFDocument data={salesData} />).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${salesData?.so_number || 'sales-order'}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
}
};
if (!salesData) {
return (
<div className='flex items-center justify-center min-h-screen'>
<div className='text-gray-500'>No sales order data available</div>
</div>
);
}
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
onClick={handleDownloadPDF}
isLoading={isGeneratingPDF}
>
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
{salesData.so_number}
</Button>
) : null;
};
export default SalesOrderExport;
const PDFDocument = ({ data }: { data: Marketing }) => {
const grandTotal = useMemo(() => {
return data?.sales_order?.reduce((a, b) => a + b.total_price, 0) ?? 0;
}, [data?.sales_order]);
return (
<Document>
<Page size='A4' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
{/* Sales Order Title */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.title}>SALES ORDER</Text>
<View style={pdfStyles.poInfo}>
<Text>SO Number: {data?.so_number || '-'}</Text>
<Text>
Date:{' '}
{data?.so_date
? formatDate(data.so_date, 'DD MMM YYYY')
: formatDate(new Date(), 'DD MMM YYYY')}
</Text>
</View>
</View>
{/* Customer Table */}
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Customer</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Sales</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
</Text>
<Text style={{ marginTop: '2px' }}>
{data?.customer.email || ''} - {data?.customer.phone || ''}
</Text>
<Text></Text>
<Text>{data?.customer.address || ''}</Text>
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
</View>
<View style={pdfStyles.tableCellLast}>
<Text style={{ fontWeight: 'bold' }}>
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={{ fontWeight: 'bold', marginTop: '2px' }}>
{data?.sales_person?.name || '-'}
</Text>
<Text>{data?.sales_person.email}</Text>
</View>
</View>
</View>
{/* Product Sales Order Table */}
<Text style={pdfStyles.sectionTitle}>Product Sold</Text>
<View style={pdfStyles.table}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeader}>
<Text>Item Description</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>From</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Unit Price</Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text>Quantity</Text>
</View>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Total Amount</Text>
</View>
</View>
{data?.sales_order?.map((item, index) => {
const isLastItem = index === (data?.sales_order?.length || 0) - 1;
return (
<View
key={index}
style={[
pdfStyles.tableRow,
// isLastItem ? {} : pdfStyles.tableBorderBottom,
]}
>
<View style={pdfStyles.tableCell}>
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text>{item.product_warehouse?.warehouse?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text>{formatNumber(item.qty || 0)}</Text>
</View>
<View style={pdfStyles.tableCellRightLast}>
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
</View>
</View>
);
}) || []}
{/* Grand Total Row inside table */}
<View style={pdfStyles.grandTotalRow}>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRight}>
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
</View>
<View
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
>
<Text>Rp{formatNumber(grandTotal)}</Text>
</View>
</View>
</View>
{/* Footer with Special Instructions */}
<View style={pdfStyles.footer}>
<View style={pdfStyles.specialInstructionTable}>
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellHeaderLast}>
<Text>Notes</Text>
</View>
</View>
<View style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCellLast}>
<Text>{data?.notes || '-'}</Text>
</View>
</View>
</View>
<View style={pdfStyles.footerCompany}>
<Text>PT LUMBUNG TELUR INDONESIA</Text>
</View>
</View>
</Page>
</Document>
);
};
@@ -0,0 +1,212 @@
import { StyleSheet } from '@react-pdf/renderer';
const pdfStyles = StyleSheet.create({
page: {
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
header: {
marginBottom: 20,
},
logo: {
width: 120,
height: 30,
marginBottom: 8,
},
companyInfo: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
color: '#1f74bf',
},
address: {
fontSize: 8,
color: '#666666',
maxWidth: 400,
marginBottom: 10,
},
divider: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
marginBottom: 15,
},
titleSection: {
flexDirection: 'row',
marginBottom: 20,
justifyContent: 'space-between',
alignItems: 'flex-start',
},
title: {
fontSize: 18,
fontWeight: 'bold',
flex: 3,
color: '#1f74bf',
},
poInfo: {
flex: 1,
fontSize: 9,
textAlign: 'right',
},
sectionTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
table: {
borderWidth: 1,
borderColor: '#000000',
marginBottom: 15,
},
tableRow: {
flexDirection: 'row',
},
tableHeader: {
backgroundColor: '#F5F5F5',
},
tableCell: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
},
tableCellLast: {
flex: 1,
padding: 8,
fontSize: 9,
},
tableCellHeader: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
},
tableCellHeaderLast: {
flex: 1,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
},
tableCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
textAlign: 'right',
},
tableCellRightLast: {
flex: 1,
padding: 8,
fontSize: 9,
textAlign: 'right',
},
tableBorderBottom: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
grandTotalRow: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#000000',
borderTopStyle: 'solid',
},
grandTotalLabel: {
flex: 3,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
},
grandTotalValue: {
flex: 1,
padding: 8,
fontSize: 9,
fontWeight: 'bold',
textAlign: 'right',
borderRightWidth: 0,
},
allocationSection: {
marginBottom: 15,
},
allocationTable: {
borderWidth: 1,
borderColor: '#000000',
},
innerTable: {
marginTop: 5,
borderWidth: 1,
borderColor: '#000000',
},
innerRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
innerCell: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
},
innerCellLast: {
flex: 1,
padding: 8,
fontSize: 9,
},
innerCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 8,
fontSize: 9,
textAlign: 'right',
},
innerCellRightLast: {
flex: 1,
padding: 8,
fontSize: 9,
textAlign: 'right',
},
footer: {
marginTop: 30,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
footerCompany: {
fontSize: 12,
fontWeight: 'bold',
textAlign: 'right',
flex: 1,
color: '#1f74bf',
},
specialInstructionTable: {
width: '60%',
maxWidth: 300,
borderWidth: 1,
borderColor: '#000000',
flex: 1,
},
});
export default pdfStyles;
+1 -1
View File
@@ -48,7 +48,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
{
title: 'Penjualan',
link: '/marketing/sales-orders',
link: '/marketing',
icon: 'mdi:attach-money',
},