#
tokens: 15929/50000 3/85 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/ralfarishi/cheese-stick-koe-dashboard?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── components.json
├── eslint.config.mjs
├── jsconfig.json
├── middleware.js
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── logo.png
│   ├── next.svg
│   ├── qris.png
│   ├── vercel.svg
│   └── window.svg
├── README.md
└── src
    ├── app
    │   ├── _components
    │   │   └── LoginForm.jsx
    │   ├── api
    │   │   └── logout
    │   │       └── route.js
    │   ├── dashboard
    │   │   ├── invoices
    │   │   │   ├── _components
    │   │   │   │   ├── DeleteInvoiceModal.jsx
    │   │   │   │   ├── InvoiceDownloadModal.jsx
    │   │   │   │   ├── InvoicePreview.jsx
    │   │   │   │   └── StatusCombobox.jsx
    │   │   │   ├── [invoiceNumber]
    │   │   │   │   └── page.jsx
    │   │   │   ├── create
    │   │   │   │   ├── _components
    │   │   │   │   │   ├── ProductsCombobox.jsx
    │   │   │   │   │   └── SizeCombobox.jsx
    │   │   │   │   ├── CreateInvoicePage.jsx
    │   │   │   │   └── page.js
    │   │   │   ├── InvoicePage.jsx
    │   │   │   ├── page.js
    │   │   │   ├── Table.jsx
    │   │   │   └── UpdateInvoiceForm.jsx
    │   │   ├── layout.jsx
    │   │   ├── page.jsx
    │   │   ├── products
    │   │   │   ├── _components
    │   │   │   │   ├── ProductDeleteModal.jsx
    │   │   │   │   ├── ProductEditModal.jsx
    │   │   │   │   ├── ProductModal.jsx
    │   │   │   │   ├── ProductModalButton.jsx
    │   │   │   │   └── ProductTable.jsx
    │   │   │   ├── page.js
    │   │   │   └── ProductPage.jsx
    │   │   └── size-pricing
    │   │       ├── _components
    │   │       │   ├── AddSizeButton.jsx
    │   │       │   ├── AddSizeModal.jsx
    │   │       │   ├── DeleteSizeModal.jsx
    │   │       │   ├── EditSizeModal.jsx
    │   │       │   └── Table.jsx
    │   │       ├── page.js
    │   │       └── SizePage.jsx
    │   ├── favicon.ico
    │   ├── globals.css
    │   ├── layout.js
    │   ├── not-found.js
    │   ├── page.js
    │   └── unauthorized.js
    ├── components
    │   ├── dashboard
    │   │   ├── DatePicker.jsx
    │   │   ├── Modal.jsx
    │   │   └── Sidebar.jsx
    │   └── ui
    │       ├── badge.jsx
    │       ├── button.jsx
    │       ├── calendar.jsx
    │       ├── card.jsx
    │       ├── command.jsx
    │       ├── dialog.jsx
    │       ├── drawer.jsx
    │       ├── input.jsx
    │       ├── label.jsx
    │       ├── popover.jsx
    │       ├── radio-group.jsx
    │       ├── select.jsx
    │       └── sonner.jsx
    └── lib
        ├── actions
        │   ├── invoice
        │   │   ├── deleteInvoice.js
        │   │   ├── getAll.js
        │   │   ├── getInvoiceByNumber.js
        │   │   ├── getInvoiceWithItem.js
        │   │   ├── submitInvoice.js
        │   │   └── updateInvoice.js
        │   ├── products
        │   │   ├── addProduct.js
        │   │   ├── deleteProduct.js
        │   │   ├── getAllProducts.js
        │   │   └── updateProduct.js
        │   └── size-price
        │       ├── addSize.js
        │       ├── deleteSize.js
        │       ├── getAll.js
        │       └── updateSize.js
        ├── exportToPng.js
        ├── supabaseBrowser.js
        ├── supabaseServer.js
        ├── utils.js
        └── verifySession.js
```

# Files

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/_components/InvoicePreview.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { forwardRef, useEffect, useState } from "react";
  4 | 
  5 | import Image from "next/image";
  6 | 
  7 | import { getAllProducts } from "@/lib/actions/products/getAllProducts";
  8 | import { getAllSizePrice } from "@/lib/actions/size-price/getAll";
  9 | 
 10 | import { cn, formatDateFilename, toTitleCase } from "@/lib/utils";
 11 | 
 12 | const InvoicePreview = forwardRef(
 13 | 	(
 14 | 		{ invoice, invoiceItems, shippingType, onReady, onDataReady, isDownloadVersion = false },
 15 | 		ref
 16 | 	) => {
 17 | 		const [products, setProducts] = useState([]);
 18 | 		const [sizes, setSizes] = useState([]);
 19 | 		const [items, setItems] = useState([]);
 20 | 
 21 | 		useEffect(() => {
 22 | 			const fetchData = async () => {
 23 | 				const { data: productsData } = await getAllProducts();
 24 | 				const { data: sizeData } = await getAllSizePrice();
 25 | 
 26 | 				setProducts(productsData || []);
 27 | 				setSizes(sizeData || []);
 28 | 			};
 29 | 
 30 | 			fetchData();
 31 | 		}, []);
 32 | 
 33 | 		useEffect(() => {
 34 | 			if (invoiceItems?.length && products.length && sizes.length) {
 35 | 				const mappedItems = invoiceItems.map((item) => {
 36 | 					const product = products.find((p) => p.id === item.productId);
 37 | 					const size = sizes.find((s) => s.id === item.sizePriceId);
 38 | 					const price = size?.price || 0;
 39 | 					const discount = item.discountAmount || 0;
 40 | 					const quantity = item.quantity;
 41 | 
 42 | 					return {
 43 | 						productName: product?.name || "Unknown",
 44 | 						sizeName: size?.size || "Unknown",
 45 | 						quantity,
 46 | 						price,
 47 | 						discountAmount: discount,
 48 | 						total: quantity * price - discount,
 49 | 					};
 50 | 				});
 51 | 
 52 | 				setItems(mappedItems);
 53 | 
 54 | 				onDataReady?.(true);
 55 | 			} else {
 56 | 				onDataReady?.(false);
 57 | 			}
 58 | 		}, [invoiceItems, products, sizes]);
 59 | 
 60 | 		useEffect(() => {
 61 | 			if (!items.length) return;
 62 | 
 63 | 			const timer = setTimeout(() => {
 64 | 				onReady?.();
 65 | 			}, 0);
 66 | 
 67 | 			return () => clearTimeout(timer);
 68 | 		}, [items]);
 69 | 
 70 | 		const subtotal = items.reduce((acc, item) => acc + item.total, 0);
 71 | 		const discount = invoice.discount || 0;
 72 | 
 73 | 		const discountPercent = subtotal > 0 ? (discount / subtotal) * 100 : 0;
 74 | 
 75 | 		const TOTAL_GAP_ROWS = 10;
 76 | 		const gapRows = Math.max(0, TOTAL_GAP_ROWS - items.length);
 77 | 
 78 | 		return (
 79 | 			<>
 80 | 				<style jsx global>{`
 81 | 					@import url("https://fonts.googleapis.com/css2?family=Pattaya&family=Poppins:wght@400;700&display=swap");
 82 | 
 83 | 					.invoice-content {
 84 | 						font-family: "Poppins", sans-serif;
 85 | 					}
 86 | 
 87 | 					.thanks-msg {
 88 | 						font-family: "Pattaya", cursive;
 89 | 					}
 90 | 
 91 | 					.invoice-content * {
 92 | 						box-sizing: border-box;
 93 | 					}
 94 | 
 95 | 					.invoice-content table td,
 96 | 					.invoice-content table th {
 97 | 						padding: 8px 12px; /* ~px-3 py-2 */
 98 | 						white-space: nowrap;
 99 | 					}
100 | 				`}</style>
101 | 
102 | 				<div
103 | 					ref={ref}
104 | 					className={cn(
105 | 						"border border-[#6D2315] font-sans text-sm text-gray-900 invoice-content",
106 | 						isDownloadVersion
107 | 							? "w-[1080px] p-3 overflow-visible"
108 | 							: "w-full md:w-[1080px] overflow-x-auto p-4"
109 | 					)}
110 | 				>
111 | 					<div
112 | 						className={cn("flex w-full", isDownloadVersion ? "flex-row" : "flex-col md:flex-row")}
113 | 					>
114 | 						{/* LEFT SECTION */}
115 | 						<div
116 | 							className={cn(
117 | 								"pr-4 border-r border-[#6D2315] space-y-6",
118 | 								isDownloadVersion ? "w-1/3" : "w-full md:w-1/3"
119 | 							)}
120 | 						>
121 | 							<div className="flex justify-center mb-4">
122 | 								<div className="w-24 h-24 relative">
123 | 									<Image src="/logo.png" alt="Logo" fill className="object-cover rounded-full" />
124 | 								</div>
125 | 							</div>
126 | 
127 | 							<h2 className="text-center font-bold text-[#6b1d1d] uppercase text-lg pb-7">
128 | 								cheese stick koe
129 | 							</h2>
130 | 
131 | 							<div>
132 | 								<h4 className="text-[#6b1d1d] font-semibold text-xs mb-1 uppercase">
133 | 									diberikan kepada
134 | 								</h4>
135 | 								<p className="text-sm">{toTitleCase(invoice?.buyerName)}</p>
136 | 							</div>
137 | 
138 | 							<div>
139 | 								<h4 className="text-[#6b1d1d] font-semibold text-xs mb-1 uppercase">detail bank</h4>
140 | 								<div className="text-sm space-y-2">
141 | 									<div>
142 | 										<span className="font-semibold uppercase">bri</span>
143 | 										<br />
144 | 										Ermi Sayekti Endahwati
145 | 										<br />
146 | 										0122-01-012734-53-8
147 | 									</div>
148 | 									<div>
149 | 										<span className="font-semibold uppercase">bca</span>
150 | 										<br />
151 | 										Ermi Sayekti Endahwati
152 | 										<br />
153 | 										524-5031-928
154 | 									</div>
155 | 								</div>
156 | 							</div>
157 | 
158 | 							<figure className="space-y-2">
159 | 								{/* Image */}
160 | 								<figcaption className="text-xs text-left font-bold italic">
161 | 									QRIS a.n Cheese Stick Koe
162 | 								</figcaption>
163 | 
164 | 								<div className="flex justify-center mt-5">
165 | 									<div className="relative w-38 h-38">
166 | 										<Image src="/qris.png" alt="Icon" fill className="object-contain" />
167 | 									</div>
168 | 								</div>
169 | 
170 | 								{/* Caption */}
171 | 							</figure>
172 | 
173 | 							{/* Thank You Text */}
174 | 							<div className="text-center pt-5 font-bold text-2xl text-[#6b1d1d] thanks-msg uppercase">
175 | 								terima kasih
176 | 							</div>
177 | 						</div>
178 | 
179 | 						{/* RIGHT SECTION */}
180 | 						<div className="md:w-2/3 w-full pl-6 space-y-6 mt-6 md:mt-0">
181 | 							<div className="text-center border-b border-[#6D2315] pb-2">
182 | 								<h1 className="font-bold text-lg uppercase">invoice pembayaran</h1>
183 | 								<div className="flex justify-between text-xs mt-1">
184 | 									<span>Invoice No. {invoice?.invoiceNumber}</span>
185 | 									<span className="whitespace-nowrap">
186 | 										{formatDateFilename(invoice?.invoiceDate)}
187 | 									</span>
188 | 								</div>
189 | 							</div>
190 | 
191 | 							<div
192 | 								className={cn(
193 | 									!isDownloadVersion && "overflow-x-auto max-w-full",
194 | 									isDownloadVersion && "overflow-hidden"
195 | 								)}
196 | 							>
197 | 								<table
198 | 									className="text-left text-sm"
199 | 									style={{
200 | 										border: "1.5px solid #6D2315",
201 | 										tableLayout: "auto",
202 | 										width: "100%",
203 | 									}}
204 | 								>
205 | 									<thead
206 | 										style={{ backgroundColor: "white", borderBottom: "1.5px solid #6D2315" }}
207 | 										className="uppercase"
208 | 									>
209 | 										<tr>
210 | 											<th className="px-2.5 py-2 whitespace-nowrap">item</th>
211 | 											<th className="px-2.5 py-2 whitespace-nowrap">ukuran</th>
212 | 											<th className="px-2.5 py-2 whitespace-nowrap">jml</th>
213 | 											<th className="px-2.5 py-2 whitespace-nowrap">harga</th>
214 | 											<th className="px-2.5 py-2 whitespace-nowrap">diskon</th>
215 | 											<th className="px-2.5 py-2 whitespace-nowrap">total</th>
216 | 										</tr>
217 | 									</thead>
218 | 									<tbody>
219 | 										{items.map((item, i) => (
220 | 											<tr key={i}>
221 | 												<td className="px-2.5 py-2 whitespace-nowrap">{item.productName}</td>
222 | 												<td className="px-2.5 py-2 whitespace-nowrap text-center">
223 | 													{item.sizeName}
224 | 												</td>
225 | 												<td className="px-2.5 py-2 whitespace-nowrap text-center">
226 | 													{item.quantity}
227 | 												</td>
228 | 												<td className="px-2.5 py-2 whitespace-nowrap">{`Rp ${item.price.toLocaleString(
229 | 													"id-ID"
230 | 												)}`}</td>
231 | 												<td
232 | 													className={`px-2.5 py-2 whitespace-nowrap text-center ${
233 | 														item.discountAmount > 0 ? "text-green-600" : ""
234 | 													}`}
235 | 												>
236 | 													{item.discountAmount
237 | 														? `-Rp ${item.discountAmount.toLocaleString("id-ID")}`
238 | 														: "-"}
239 | 												</td>
240 | 												<td className="p-1 whitespace-nowrap">
241 | 													{item.total ? `Rp ${item.total.toLocaleString("id-ID")}` : "-"}
242 | 												</td>
243 | 											</tr>
244 | 										))}
245 | 
246 | 										{Array.from({ length: gapRows }).map((_, idx) => (
247 | 											<tr key={`gap-${idx}`}>
248 | 												<td>&nbsp;</td>
249 | 												<td></td>
250 | 												<td></td>
251 | 												<td></td>
252 | 												<td></td>
253 | 												<td></td>
254 | 												<td></td>
255 | 											</tr>
256 | 										))}
257 | 
258 | 										{/* Subtotal */}
259 | 										<tr style={{ borderTop: "1.5px solid #6D2315" }}>
260 | 											<td colSpan="5" className="px-2.5 py-2 text-right uppercase">
261 | 												Sub Total :
262 | 											</td>
263 | 											<td className="px-2.5 py-2 whitespace-nowrap">
264 | 												Rp {subtotal.toLocaleString("id-ID")}
265 | 											</td>
266 | 										</tr>
267 | 
268 | 										{/* Diskon */}
269 | 										{invoice?.discount > 0 && (
270 | 											<tr className="text-green-500">
271 | 												<td colSpan="5" className="px-2.5 py-2 text-right uppercase">
272 | 													diskon ({discountPercent.toFixed(2)}%) :
273 | 												</td>
274 | 												<td className="px-2.5 py-2 whitespace-nowrap">
275 | 													-Rp{" "}
276 | 													{typeof invoice.discount === "number"
277 | 														? invoice.discount.toLocaleString("id-ID")
278 | 														: "0"}
279 | 												</td>
280 | 											</tr>
281 | 										)}
282 | 
283 | 										{/* Ongkir */}
284 | 										<tr>
285 | 											<td colSpan="5" className="px-2.5 py-2 text-right uppercase">
286 | 												{shippingType ? `ongkir (${shippingType}) :` : "ongkir :"}
287 | 											</td>
288 | 											<td className="px-2.5 py-2 whitespace-nowrap">
289 | 												Rp {invoice?.shipping?.toLocaleString("id-ID")}
290 | 											</td>
291 | 										</tr>
292 | 
293 | 										{/* Total */}
294 | 										<tr style={{ borderBottom: "1.5px solid #6D2315" }}>
295 | 											<td
296 | 												colSpan="5"
297 | 												className="px-2.5 py-2 text-right font-bold text-red-700 uppercase"
298 | 											>
299 | 												jumlah yang harus dibayar :
300 | 											</td>
301 | 											<td className="px-2.5 py-2 font-bold text-red-700 whitespace-nowrap">
302 | 												Rp{" "}
303 | 												{typeof invoice.totalPrice === "number"
304 | 													? invoice.totalPrice.toLocaleString("id-ID")
305 | 													: "0"}
306 | 											</td>
307 | 										</tr>
308 | 									</tbody>
309 | 								</table>
310 | 							</div>
311 | 
312 | 							{/* Disclaimer */}
313 | 							<div className="text-red-600 pt-4 border-t border-gray-200">
314 | 								<span className="font-semibold text-sm">*Disclaimer</span>
315 | 								<br />
316 | 								<span className="text-black text-sm">
317 | 									Segala kerusakan yang terjadi selama pengiriman menjadi tanggung jawab pihak
318 | 									ekspedisi. Namun, kami siap membantu proses klaim ke pihak ekspedisi apabila
319 | 									terjadi kendala selama pengiriman.
320 | 								</span>
321 | 							</div>
322 | 						</div>
323 | 					</div>
324 | 				</div>
325 | 			</>
326 | 		);
327 | 	}
328 | );
329 | 
330 | export default InvoicePreview;
331 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/create/CreateInvoicePage.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useEffect, useState } from "react";
  4 | 
  5 | import Link from "next/link";
  6 | 
  7 | import { Controller, useForm } from "react-hook-form";
  8 | 
  9 | import { supabaseBrowser } from "@/lib/supabaseBrowser";
 10 | 
 11 | import { Input } from "@/components/ui/input";
 12 | import { Button } from "@/components/ui/button";
 13 | import { Label } from "@/components/ui/label";
 14 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 15 | 
 16 | import DatePicker from "@/components/dashboard/DatePicker";
 17 | 
 18 | import ProductCombobox from "./_components/ProductsCombobox";
 19 | import SizeCombobox from "./_components/SizeCombobox";
 20 | 
 21 | import { ChevronRight, Trash2 } from "lucide-react";
 22 | import { toast } from "sonner";
 23 | 
 24 | import { getAllProducts } from "@/lib/actions/products/getAllProducts";
 25 | import { getAllSizePrice } from "@/lib/actions/size-price/getAll";
 26 | import { submitInvoice } from "@/lib/actions/invoice/submitInvoice";
 27 | import { calculateDiscountAmount, calculateDiscountPercent } from "@/lib/utils";
 28 | 
 29 | export default function CreateInvoicePage() {
 30 | 	const supabase = supabaseBrowser();
 31 | 	const [user, setUser] = useState(null);
 32 | 
 33 | 	const [products, setProducts] = useState([]);
 34 | 	const [sizes, setSizes] = useState([]);
 35 | 
 36 | 	const [invoiceDate, setInvoiceDate] = useState(new Date().toISOString());
 37 | 
 38 | 	const [lastInvoiceNumber, setLastInvoiceNumber] = useState(null);
 39 | 
 40 | 	const [shippingPrice, setShippingPrice] = useState(0);
 41 | 
 42 | 	const {
 43 | 		control,
 44 | 		handleSubmit,
 45 | 		formState: { errors },
 46 | 		reset,
 47 | 	} = useForm({
 48 | 		defaultValues: {
 49 | 			invoiceNumber: "",
 50 | 			buyerName: "",
 51 | 		},
 52 | 		mode: "onChange",
 53 | 	});
 54 | 
 55 | 	const resetForm = () => {
 56 | 		setInvoiceDate(new Date().toISOString());
 57 | 		setShippingPrice(0);
 58 | 		setDiscountInput(0);
 59 | 		setItems([createEmptyItem()]);
 60 | 	};
 61 | 
 62 | 	const createEmptyItem = () => ({
 63 | 		productId: "",
 64 | 		sizePriceId: "",
 65 | 		quantity: 1,
 66 | 		price: 0,
 67 | 		discountMode: "amount" | "percent",
 68 | 		discountInput: "",
 69 | 		discountAmount: 0,
 70 | 		total: 0,
 71 | 	});
 72 | 
 73 | 	const [items, setItems] = useState([createEmptyItem()]);
 74 | 
 75 | 	// general discount
 76 | 	const [discountMode, setDiscountMode] = useState("amount");
 77 | 	const [discountInput, setDiscountInput] = useState(0);
 78 | 
 79 | 	useEffect(() => {
 80 | 		const fetchInitialData = async () => {
 81 | 			const [{ data: userData }, { data: lastInvoice }] = await Promise.all([
 82 | 				supabase.auth.getUser(),
 83 | 				supabase
 84 | 					.from("Invoice")
 85 | 					.select("invoiceNumber, invoiceDate")
 86 | 					.order("invoiceDate", { ascending: false })
 87 | 					.limit(1)
 88 | 					.single(),
 89 | 			]);
 90 | 
 91 | 			if (userData?.user) setUser(userData.user);
 92 | 			if (lastInvoice) setLastInvoiceNumber(lastInvoice.invoiceNumber);
 93 | 		};
 94 | 
 95 | 		fetchInitialData();
 96 | 	}, []);
 97 | 
 98 | 	const addItem = () => {
 99 | 		setItems([...items, createEmptyItem()]);
100 | 	};
101 | 
102 | 	const removeItem = (index) => {
103 | 		setItems((prev) => prev.filter((_, i) => i !== index));
104 | 	};
105 | 
106 | 	useEffect(() => {
107 | 		const fetchOptions = async () => {
108 | 			const [{ data: prods }, { data: szs }] = await Promise.all([
109 | 				getAllProducts(),
110 | 				getAllSizePrice(),
111 | 			]);
112 | 
113 | 			setProducts(prods || []);
114 | 			setSizes(szs || []);
115 | 		};
116 | 
117 | 		fetchOptions();
118 | 	}, []);
119 | 
120 | 	// calculate each item total and subtotal
121 | 	const updateItemField = (item, field, value, mode = null) => {
122 | 		if (field === "sizePriceId") {
123 | 			const selectedSize = sizes.find((s) => s.id === value);
124 | 			item.sizePriceId = value;
125 | 			item.price = selectedSize?.price || 0;
126 | 		} else if (field === "quantity") {
127 | 			const parsed = parseInt(value, 10);
128 | 			item.quantity = isNaN(parsed) || parsed < 1 ? 1 : parsed;
129 | 		} else if (field === "price") {
130 | 			const parsed = parseInt(value, 10);
131 | 			item.price = isNaN(parsed) ? 0 : parsed;
132 | 		} else if (field === "discountMode") {
133 | 			item.discountMode = value;
134 | 		} else if (field === "discountInput") {
135 | 			item.discountInput = value;
136 | 			item.discountMode = mode;
137 | 		} else {
138 | 			item[field] = value;
139 | 		}
140 | 
141 | 		const qty = item.quantity || 0;
142 | 		const price = item.price || 0;
143 | 		const rawTotal = qty * price;
144 | 
145 | 		const discountAmount = calculateDiscountAmount({
146 | 			quantity: item.quantity,
147 | 			price: item.price,
148 | 			discountInput: item.discountInput,
149 | 			discountMode: item.discountMode,
150 | 		});
151 | 
152 | 		item.discountAmount = discountAmount;
153 | 		item.total = rawTotal - discountAmount;
154 | 	};
155 | 
156 | 	const handleItemChange = (index, field, value, mode = null) => {
157 | 		const updatedItems = [...items];
158 | 		const item = updatedItems[index];
159 | 
160 | 		updateItemField(item, field, value, mode);
161 | 		setItems(updatedItems);
162 | 	};
163 | 
164 | 	const subtotal = items.reduce((sum, item) => sum + item.total, 0);
165 | 
166 | 	const discountAmount =
167 | 		discountMode === "percent"
168 | 			? Math.round(((parseFloat(discountInput) || 0) / 100) * subtotal)
169 | 			: parseInt(discountInput) || 0;
170 | 
171 | 	const discountPercent =
172 | 		discountMode === "amount"
173 | 			? ((parseInt(discountInput) || 0) / subtotal) * 100
174 | 			: parseFloat(discountInput) || 0;
175 | 
176 | 	const totalPrice = subtotal + (parseInt(shippingPrice) || 0) - discountAmount;
177 | 
178 | 	const onSubmit = async (data) => {
179 | 		if (!user) {
180 | 			toast.error("User not log in");
181 | 			return;
182 | 		}
183 | 
184 | 		const isInvalid = items.some((item) => !item.productId || !item.sizePriceId);
185 | 
186 | 		if (isInvalid) {
187 | 			toast.error("You must add product and size before submitting!");
188 | 			return;
189 | 		}
190 | 
191 | 		const res = await submitInvoice({
192 | 			invoiceNumber: data.invoiceNumber,
193 | 			buyerName: data.buyerName.trim().toLowerCase(),
194 | 			invoiceDate,
195 | 			shippingPrice,
196 | 			discountAmount,
197 | 			totalPrice,
198 | 			items,
199 | 			user,
200 | 		});
201 | 
202 | 		if (res.error) {
203 | 			toast.error(res.error);
204 | 		} else {
205 | 			toast.success(res.message);
206 | 
207 | 			reset({
208 | 				invoiceNumber: "",
209 | 				buyerName: "",
210 | 			});
211 | 
212 | 			resetForm();
213 | 		}
214 | 	};
215 | 
216 | 	return (
217 | 		<section className="w-full px-4 py-6 bg-[#fffaf0]">
218 | 			<div className="bg-white rounded-xl shadow-md p-6 space-y-6 border border-[#f4e3d3]">
219 | 				{/* Breadcrumbs (Mobile Only) */}
220 | 				<div className="block md:hidden text-sm text-gray-500 mb-4">
221 | 					<nav className="flex items-center space-x-1">
222 | 						<Link className="text-gray-400" href="/dashboard/invoices">
223 | 							List Invoice
224 | 						</Link>
225 | 						<ChevronRight className="w-4 h-4 text-gray-400" />
226 | 						<span className="text-gray-700 font-medium">Create Invoice</span>
227 | 					</nav>
228 | 				</div>
229 | 
230 | 				<Card className="border-0 shadow-none">
231 | 					<CardHeader className="text-center mb-2">
232 | 						<CardTitle className="font-bold text-3xl text-[#6D2315]">INVOICE</CardTitle>
233 | 					</CardHeader>
234 | 
235 | 					<CardContent>
236 | 						<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
237 | 							{/* Basic Info */}
238 | 							<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
239 | 								<div>
240 | 									<Label className="py-2 block text-sm text-gray-700">Invoice Number</Label>
241 | 									<Controller
242 | 										name="invoiceNumber"
243 | 										control={control}
244 | 										rules={{
245 | 											required: "Invoice Number is required!",
246 | 											pattern: {
247 | 												value: /^\d{4}$/,
248 | 												message: "Invoice Number must be exactly 4 digits (0-9)",
249 | 											},
250 | 										}}
251 | 										render={({ field }) => (
252 | 											<Input
253 | 												{...field}
254 | 												placeholder={`Nomor invoice terakhir: ${lastInvoiceNumber || "0000"}`}
255 | 												maxLength={4}
256 | 												required
257 | 											/>
258 | 										)}
259 | 									/>
260 | 									{errors.invoiceNumber && (
261 | 										<p role="alert" className="text-sm text-red-500">
262 | 											{errors.invoiceNumber.message}
263 | 										</p>
264 | 									)}
265 | 								</div>
266 | 								<div>
267 | 									<Label className="py-2 block text-sm text-gray-700">Buyer Name</Label>
268 | 									<Controller
269 | 										name="buyerName"
270 | 										control={control}
271 | 										rules={{
272 | 											required: "Buyer Name is required!",
273 | 											pattern: {
274 | 												value: /^[A-Za-z\s]+$/,
275 | 												message: "Buyer Name must contain only letters and spaces",
276 | 											},
277 | 										}}
278 | 										render={({ field }) => <Input {...field} placeholder="Nama pembeli" required />}
279 | 									/>
280 | 									{errors.buyerName && (
281 | 										<p role="alert" className="text-sm text-red-500">
282 | 											{errors.buyerName.message}
283 | 										</p>
284 | 									)}
285 | 								</div>
286 | 								<div>
287 | 									<Label className="py-2 block text-sm text-gray-700">Invoice Date</Label>
288 | 									<DatePicker invoiceDate={invoiceDate} setInvoiceDate={setInvoiceDate} />
289 | 								</div>
290 | 							</div>
291 | 
292 | 							{/* Item List */}
293 | 							<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
294 | 								{items.map((item, index) => (
295 | 									<div key={index}>
296 | 										<h3 className="text-sm font-medium text-[#6D2315] mb-1">Item {index + 1}</h3>
297 | 
298 | 										<div className="bg-[#fffefb] border border-[#fceee4] rounded-md p-4 space-y-3">
299 | 											{/* Item Select */}
300 | 											<div>
301 | 												<Label className="text-sm text-gray-700 mb-1 block">Item</Label>
302 | 												<ProductCombobox
303 | 													products={products}
304 | 													value={item.productId}
305 | 													onChange={(val) => handleItemChange(index, "productId", val)}
306 | 												/>
307 | 											</div>
308 | 
309 | 											{/* Size & Qty */}
310 | 											<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
311 | 												<div>
312 | 													<Label className="text-sm text-gray-700 mb-1 block">Size</Label>
313 | 													<SizeCombobox
314 | 														sizes={sizes}
315 | 														value={item.sizePriceId}
316 | 														onChange={(val, price) => {
317 | 															handleItemChange(index, "sizePriceId", val);
318 | 															handleItemChange(index, "price", price);
319 | 														}}
320 | 													/>
321 | 												</div>
322 | 												<div>
323 | 													<Label className="text-sm text-gray-700 mb-1 block">Qty</Label>
324 | 													{/* desktop */}
325 | 													<div className="hidden md:block">
326 | 														<Input
327 | 															type="number"
328 | 															value={item.quantity}
329 | 															onChange={(e) => handleItemChange(index, "quantity", e.target.value)}
330 | 															required
331 | 														/>
332 | 													</div>
333 | 
334 | 													<div className="flex items-center gap-2 md:hidden">
335 | 														<Input
336 | 															type="number"
337 | 															value={item.quantity}
338 | 															onChange={(e) => handleItemChange(index, "quantity", e.target.value)}
339 | 															className="w-15 text-center"
340 | 														/>
341 | 														<button
342 | 															type="button"
343 | 															className="w-15 px-2 py-1 border rounded bg-rose-500 text-white"
344 | 															onClick={() =>
345 | 																handleItemChange(
346 | 																	index,
347 | 																	"quantity",
348 | 																	Math.max(1, Number(item.quantity) - 1)
349 | 																)
350 | 															}
351 | 														>
352 | 															-
353 | 														</button>
354 | 														<button
355 | 															type="button"
356 | 															className="w-15 px-2 py-1 border rounded bg-emerald-500 text-white"
357 | 															onClick={() =>
358 | 																handleItemChange(index, "quantity", Number(item.quantity) + 1)
359 | 															}
360 | 														>
361 | 															+
362 | 														</button>
363 | 													</div>
364 | 												</div>
365 | 											</div>
366 | 
367 | 											{/* Price & Total */}
368 | 											<div className="grid grid-cols-2 gap-2">
369 | 												<div>
370 | 													<Label className="text-sm text-gray-700 mb-1 block">Price</Label>
371 | 													<Input
372 | 														type="number"
373 | 														value={item.price}
374 | 														disabled
375 | 														className="bg-gray-100"
376 | 													/>
377 | 												</div>
378 | 												<div>
379 | 													<Label className="text-sm text-gray-700 mb-1 block">Total</Label>
380 | 													<Input
381 | 														value={item.total.toLocaleString("id-ID")}
382 | 														disabled
383 | 														className="bg-gray-100"
384 | 													/>
385 | 												</div>
386 | 											</div>
387 | 
388 | 											{/* Discount Each Item*/}
389 | 											<div>
390 | 												<Label className="text-sm text-gray-700 mb-1 block">
391 | 													Discount (Optional)
392 | 												</Label>
393 | 												<div className="grid grid-cols-2 gap-2">
394 | 													<div>
395 | 														<Label className="text-xs text-gray-500">Percent (%)</Label>
396 | 														<Input
397 | 															type="number"
398 | 															placeholder="%"
399 | 															min={0}
400 | 															max={100}
401 | 															step="any"
402 | 															value={
403 | 																item.discountMode === "percent"
404 | 																	? item.discountInput
405 | 																	: calculateDiscountPercent(item)
406 | 															}
407 | 															onChange={(e) =>
408 | 																handleItemChange(index, "discountInput", e.target.value, "percent")
409 | 															}
410 | 														/>
411 | 													</div>
412 | 													<div>
413 | 														<Label className="text-xs text-gray-500">Amount (Rp)</Label>
414 | 														<Input
415 | 															type="number"
416 | 															placeholder="Rp"
417 | 															min={0}
418 | 															value={
419 | 																item.discountMode === "amount"
420 | 																	? item.discountInput
421 | 																	: item.discountAmount
422 | 															}
423 | 															onChange={(e) =>
424 | 																handleItemChange(index, "discountInput", e.target.value, "amount")
425 | 															}
426 | 														/>
427 | 													</div>
428 | 												</div>
429 | 											</div>
430 | 
431 | 											{/* Delete button */}
432 | 											<div className="flex justify-end">
433 | 												<Button
434 | 													type="button"
435 | 													variant="destructive"
436 | 													onClick={() => removeItem(index)}
437 | 													className="h-9 px-3"
438 | 												>
439 | 													<Trash2 className="w-4 h-4" />
440 | 												</Button>
441 | 											</div>
442 | 										</div>
443 | 									</div>
444 | 								))}
445 | 
446 | 								{/* Add Item Button */}
447 | 								<div className="md:col-span-3">
448 | 									<Button
449 | 										type="button"
450 | 										onClick={addItem}
451 | 										className="mt-2 bg-[#6D2315] hover:bg-[#591c10] text-white"
452 | 									>
453 | 										+ Add Item
454 | 									</Button>
455 | 								</div>
456 | 							</div>
457 | 
458 | 							<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
459 | 								{/* Discount General */}
460 | 								<div className="md:col-span-4">
461 | 									<div className="bg-[#fffaf0] border border-[#f4e3d3] rounded-md px-4 py-3 h-full">
462 | 										<Label className="block text-sm text-gray-700 mb-2">Discount (Optional)</Label>
463 | 										<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
464 | 											<div>
465 | 												<Label className="text-xs text-gray-500 mb-1 block">Percent (%)</Label>
466 | 												<Input
467 | 													type="number"
468 | 													min={0}
469 | 													max={100}
470 | 													step="any"
471 | 													value={
472 | 														discountMode === "percent"
473 | 															? discountInput
474 | 															: discountPercent.toFixed(2) || 0
475 | 													}
476 | 													onChange={(e) => {
477 | 														setDiscountMode("percent");
478 | 														setDiscountInput(e.target.value);
479 | 													}}
480 | 												/>
481 | 											</div>
482 | 
483 | 											<div>
484 | 												<Label className="text-xs text-gray-500 mb-1 block">Amount (Rp)</Label>
485 | 												<Input
486 | 													type="number"
487 | 													min={0}
488 | 													value={discountMode === "amount" ? discountInput : discountAmount}
489 | 													onChange={(e) => {
490 | 														setDiscountMode("amount");
491 | 														setDiscountInput(e.target.value);
492 | 													}}
493 | 												/>
494 | 											</div>
495 | 										</div>
496 | 									</div>
497 | 								</div>
498 | 
499 | 								<div className="md:col-span-8 hidden md:block"></div>
500 | 
501 | 								{/* Subtotal */}
502 | 								<div className="md:col-span-4">
503 | 									<Label className="py-2 block text-sm text-gray-700">Subtotal</Label>
504 | 									<Input
505 | 										value={subtotal.toLocaleString("id-ID")}
506 | 										disabled
507 | 										className="bg-gray-100"
508 | 									/>
509 | 								</div>
510 | 
511 | 								{/* Shipping */}
512 | 								<div className="md:col-span-4">
513 | 									<Label className="py-2 block text-sm text-gray-700">Shipping Price</Label>
514 | 									<Input
515 | 										type="number"
516 | 										value={shippingPrice}
517 | 										onChange={(e) => setShippingPrice(e.target.value)}
518 | 									/>
519 | 								</div>
520 | 
521 | 								{/* Total */}
522 | 								<div className="md:col-span-4">
523 | 									<Label className="py-2 block text-sm text-gray-700">Total Price</Label>
524 | 									<Input
525 | 										value={totalPrice.toLocaleString("id-ID")}
526 | 										disabled
527 | 										className="bg-gray-100"
528 | 									/>
529 | 								</div>
530 | 							</div>
531 | 
532 | 							<Button type="submit" className="w-full bg-[#6D2315] hover:bg-[#591c10] text-white">
533 | 								Create Invoice
534 | 							</Button>
535 | 						</form>
536 | 					</CardContent>
537 | 				</Card>
538 | 			</div>
539 | 		</section>
540 | 	);
541 | }
542 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/UpdateInvoiceForm.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useState, useEffect } from "react";
  4 | 
  5 | import { useRouter } from "next/navigation";
  6 | import Link from "next/link";
  7 | 
  8 | import { Controller, useForm } from "react-hook-form";
  9 | 
 10 | import { Button } from "@/components/ui/button";
 11 | import { Input } from "@/components/ui/input";
 12 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 13 | import { Label } from "@/components/ui/label";
 14 | 
 15 | import DatePicker from "@/components/dashboard/DatePicker";
 16 | 
 17 | import ProductCombobox from "./create/_components/ProductsCombobox";
 18 | import SizeCombobox from "./create/_components/SizeCombobox";
 19 | import StatusCombobox from "./_components/StatusCombobox";
 20 | 
 21 | import { getAllProducts } from "@/lib/actions/products/getAllProducts";
 22 | import { getAllSizePrice } from "@/lib/actions/size-price/getAll";
 23 | import { updateInvoice } from "@/lib/actions/invoice/updateInvoice";
 24 | 
 25 | import { calculateDiscountAmount, calculateDiscountPercent, getPageTitle } from "@/lib/utils";
 26 | import { ChevronRight, Trash2 } from "lucide-react";
 27 | 
 28 | import { toast } from "sonner";
 29 | 
 30 | export const metadata = {
 31 | 	title: getPageTitle("Invoice Edit"),
 32 | };
 33 | 
 34 | export default function UpdateInvoiceForm({ invoice }) {
 35 | 	const router = useRouter();
 36 | 
 37 | 	const [products, setProducts] = useState([]);
 38 | 	const [sizes, setSizes] = useState([]);
 39 | 
 40 | 	const [invoiceDate, setInvoiceDate] = useState(invoice.invoiceDate?.split("T")[0] || "");
 41 | 	const [items, setItems] = useState([]);
 42 | 	const [shippingPrice, setShippingPrice] = useState(invoice.shipping || 0);
 43 | 	const [status, setStatus] = useState(invoice.status || "pending");
 44 | 
 45 | 	const [discountMode, setDiscountMode] = useState("amount");
 46 | 	const [discountInput, setDiscountInput] = useState(0);
 47 | 
 48 | 	const {
 49 | 		control,
 50 | 		handleSubmit,
 51 | 		formState: { errors },
 52 | 	} = useForm({
 53 | 		defaultValues: {
 54 | 			invoiceNumber: invoice.invoiceNumber || "",
 55 | 			buyerName: invoice.buyerName || "",
 56 | 		},
 57 | 		mode: "onChange",
 58 | 	});
 59 | 
 60 | 	useEffect(() => {
 61 | 		const fetchData = async () => {
 62 | 			const { data: productsData } = await getAllProducts();
 63 | 			const { data: sizeData } = await getAllSizePrice();
 64 | 
 65 | 			setProducts(productsData || []);
 66 | 			setSizes(sizeData || []);
 67 | 		};
 68 | 
 69 | 		fetchData();
 70 | 	}, []);
 71 | 
 72 | 	useEffect(() => {
 73 | 		if (invoice?.items?.length) {
 74 | 			const mappedItems = invoice.items.map((item) => {
 75 | 				const quantity = item.quantity || 0;
 76 | 				const price = item.sizePrice?.price || 0;
 77 | 				const subtotal = quantity * price;
 78 | 
 79 | 				const discountAmount = item.discountAmount || 0;
 80 | 
 81 | 				return {
 82 | 					productId: item.productId,
 83 | 					sizePriceId: item.sizePriceId,
 84 | 					quantity,
 85 | 					price,
 86 | 					discountAmount,
 87 | 					discountInput: String(discountAmount),
 88 | 					discountMode: "amount",
 89 | 					total: subtotal - discountAmount,
 90 | 				};
 91 | 			});
 92 | 
 93 | 			setItems(mappedItems);
 94 | 		}
 95 | 
 96 | 		if (invoice?.discount !== undefined) {
 97 | 			setDiscountInput(String(invoice.discount));
 98 | 			setDiscountMode("amount");
 99 | 		}
100 | 	}, [invoice]);
101 | 
102 | 	const onUpdate = async (data) => {
103 | 		const isInvalid = items.some((item) => !item.productId || !item.sizePriceId);
104 | 
105 | 		if (isInvalid) {
106 | 			toast.error("You must add product and size before submitting!");
107 | 			return;
108 | 		}
109 | 
110 | 		const result = await updateInvoice({
111 | 			invoiceId: invoice.id,
112 | 			invoiceData: {
113 | 				invoiceNumber: data.invoiceNumber,
114 | 				buyerName: data.buyerName.trim().toLowerCase(),
115 | 				invoiceDate,
116 | 				totalPrice,
117 | 				discount: discountAmount,
118 | 				shipping: parseInt(shippingPrice),
119 | 				status,
120 | 			},
121 | 			items,
122 | 		});
123 | 
124 | 		if (!result.success) {
125 | 			toast.error(result.error || "Failed to update invoice");
126 | 			return;
127 | 		}
128 | 
129 | 		toast.success("Invoice has been updated!");
130 | 		router.push("/dashboard/invoices");
131 | 	};
132 | 
133 | 	const handleItemChange = (index, field, value, mode = null) => {
134 | 		const updatedItems = [...items];
135 | 		const item = updatedItems[index];
136 | 
137 | 		if (field === "sizePriceId") {
138 | 			const selectedSize = sizes.find((s) => s.id === value);
139 | 			item.sizePriceId = value;
140 | 			item.price = selectedSize?.price || 0;
141 | 		} else if (field === "quantity") {
142 | 			const parsed = parseInt(value, 10);
143 | 			item.quantity = isNaN(parsed) || parsed < 1 ? 1 : parsed;
144 | 		} else if (field === "price") {
145 | 			const parsed = parseInt(value, 10);
146 | 			item.price = isNaN(parsed) ? 0 : parsed;
147 | 		} else if (field === "discountMode") {
148 | 			item.discountMode = value;
149 | 		} else if (field === "discountInput") {
150 | 			item.discountInput = value;
151 | 			item.discountMode = mode;
152 | 		} else {
153 | 			item[field] = value;
154 | 		}
155 | 
156 | 		const qty = item.quantity || 0;
157 | 		const price = item.price || 0;
158 | 
159 | 		const rawTotal = qty * price;
160 | 
161 | 		const discountAmount = calculateDiscountAmount({
162 | 			quantity: item.quantity,
163 | 			price: item.price,
164 | 			discountInput: item.discountInput,
165 | 			discountMode: item.discountMode,
166 | 		});
167 | 
168 | 		item.discountAmount = discountAmount;
169 | 		item.total = rawTotal - discountAmount;
170 | 
171 | 		setItems(updatedItems);
172 | 	};
173 | 
174 | 	const subtotal = items.reduce((sum, item) => sum + item.total, 0);
175 | 
176 | 	const discountAmount =
177 | 		discountMode === "percent"
178 | 			? Math.round(((parseFloat(discountInput) || 0) / 100) * subtotal)
179 | 			: parseInt(discountInput) || 0;
180 | 
181 | 	const discountPercent =
182 | 		discountMode === "amount"
183 | 			? ((parseInt(discountInput) || 0) / subtotal) * 100
184 | 			: parseFloat(discountInput) || 0;
185 | 
186 | 	const totalPrice = subtotal + (parseInt(shippingPrice) || 0) - discountAmount;
187 | 
188 | 	const addItem = () => {
189 | 		setItems([
190 | 			...items,
191 | 			{
192 | 				productId: "",
193 | 				sizePriceId: "",
194 | 				price: 0,
195 | 				quantity: 1,
196 | 				discountAmount: 0,
197 | 				discountInput: "0",
198 | 				discountMode: "amount",
199 | 				total: 0,
200 | 			},
201 | 		]);
202 | 	};
203 | 
204 | 	const removeItem = (index) => {
205 | 		const updated = [...items];
206 | 		updated.splice(index, 1);
207 | 		setItems(updated);
208 | 	};
209 | 
210 | 	return (
211 | 		<section className="w-full px-4 py-6 bg-[#fffaf0]">
212 | 			<div className="bg-white rounded-xl shadow-md p-6 space-y-6 border border-[#f4e3d3]">
213 | 				{/* Breadcrumbs (Mobile Only) */}
214 | 				<div className="block md:hidden text-sm text-gray-500 mb-4">
215 | 					<nav className="flex items-center space-x-1">
216 | 						<Link className="text-gray-400" href="/dashboard/invoices">
217 | 							List Invoice
218 | 						</Link>
219 | 						<ChevronRight className="w-4 h-4 text-gray-400" />
220 | 						<span className="text-gray-700 font-medium">Edit Invoice</span>
221 | 					</nav>
222 | 				</div>
223 | 
224 | 				<Card className="border-0 shadow-none">
225 | 					<CardHeader className="text-center mb-2">
226 | 						<CardTitle className="font-bold text-3xl text-[#6D2315]">EDIT INVOICE</CardTitle>
227 | 					</CardHeader>
228 | 
229 | 					<CardContent>
230 | 						<form onSubmit={handleSubmit(onUpdate)} className="space-y-8">
231 | 							{/* Basic Info */}
232 | 							<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
233 | 								<div>
234 | 									<Label className="py-2 block text-sm text-gray-700">Invoice Number</Label>
235 | 									<Controller
236 | 										name="invoiceNumber"
237 | 										control={control}
238 | 										rules={{
239 | 											required: "Invoice Number is required!",
240 | 											pattern: {
241 | 												value: /^\d{4}$/,
242 | 												message: "Invoice Number must be exactly 4 digits (0-9)",
243 | 											},
244 | 										}}
245 | 										render={({ field }) => <Input {...field} maxLength={4} required />}
246 | 									/>
247 | 									{errors.invoiceNumber && (
248 | 										<p role="alert" className="text-sm text-red-500">
249 | 											{errors.invoiceNumber.message}
250 | 										</p>
251 | 									)}
252 | 								</div>
253 | 								<div>
254 | 									<Label className="py-2 block text-sm text-gray-700">Buyer Name</Label>
255 | 									<Controller
256 | 										name="buyerName"
257 | 										control={control}
258 | 										rules={{
259 | 											required: "Buyer Name is required!",
260 | 											pattern: {
261 | 												value: /^[A-Za-z\s]+$/,
262 | 												message: "Buyer Name must contain only letters and spaces",
263 | 											},
264 | 										}}
265 | 										render={({ field }) => <Input {...field} placeholder="Nama pembeli" required />}
266 | 									/>
267 | 									{errors.buyerName && (
268 | 										<p role="alert" className="text-sm text-red-500">
269 | 											{errors.buyerName.message}
270 | 										</p>
271 | 									)}
272 | 								</div>
273 | 								<div>
274 | 									<Label className="py-2 block text-sm text-gray-700">Invoice Date</Label>
275 | 									<DatePicker invoiceDate={invoiceDate} setInvoiceDate={setInvoiceDate} />
276 | 								</div>
277 | 								<div className="">
278 | 									<Label className="py-2 block text-sm text-gray-700">Status</Label>
279 | 									<StatusCombobox value={status} onChange={setStatus} required />
280 | 								</div>
281 | 							</div>
282 | 
283 | 							{/* Items */}
284 | 							<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
285 | 								{items.map((item, index) => (
286 | 									<div key={index}>
287 | 										<h3 className="text-sm font-medium text-[#6D2315] mb-1">Item {index + 1}</h3>
288 | 
289 | 										<div className="bg-[#fffefb] border border-[#fceee4] rounded-md p-4 space-y-3">
290 | 											{/* Item Select */}
291 | 											<div>
292 | 												<Label className="text-sm text-gray-700 mb-1 block">Item</Label>
293 | 												<ProductCombobox
294 | 													products={products}
295 | 													value={item.productId}
296 | 													onChange={(val) => handleItemChange(index, "productId", val)}
297 | 												/>
298 | 											</div>
299 | 
300 | 											{/* Size & Qty */}
301 | 											<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
302 | 												<div>
303 | 													<Label className="text-sm text-gray-700 mb-1 block">Size</Label>
304 | 													<SizeCombobox
305 | 														sizes={sizes}
306 | 														value={item.sizePriceId}
307 | 														onChange={(val, price) => {
308 | 															handleItemChange(index, "sizePriceId", val);
309 | 															handleItemChange(index, "price", price);
310 | 														}}
311 | 													/>
312 | 												</div>
313 | 												<div>
314 | 													<Label className="text-sm text-gray-700 mb-1 block">Qty</Label>
315 | 													{/* desktop */}
316 | 													<div className="hidden md:block">
317 | 														<Input
318 | 															type="number"
319 | 															value={item.quantity}
320 | 															onChange={(e) => handleItemChange(index, "quantity", e.target.value)}
321 | 															required
322 | 														/>
323 | 													</div>
324 | 
325 | 													{/* mobile */}
326 | 													<div className="flex items-center gap-2 md:hidden">
327 | 														<Input
328 | 															type="number"
329 | 															value={item.quantity}
330 | 															onChange={(e) => handleItemChange(index, "quantity", e.target.value)}
331 | 															className="w-15 text-center"
332 | 														/>
333 | 														<button
334 | 															type="button"
335 | 															className="w-15 px-2 py-1 border rounded bg-rose-500 text-white"
336 | 															onClick={() =>
337 | 																handleItemChange(
338 | 																	index,
339 | 																	"quantity",
340 | 																	Math.max(1, Number(item.quantity) - 1)
341 | 																)
342 | 															}
343 | 														>
344 | 															-
345 | 														</button>
346 | 														<button
347 | 															type="button"
348 | 															className="w-15 px-2 py-1 border rounded bg-emerald-500 text-white"
349 | 															onClick={() =>
350 | 																handleItemChange(index, "quantity", Number(item.quantity) + 1)
351 | 															}
352 | 														>
353 | 															+
354 | 														</button>
355 | 													</div>
356 | 												</div>
357 | 											</div>
358 | 
359 | 											{/* Price & Total */}
360 | 											<div className="grid grid-cols-2 gap-2">
361 | 												<div>
362 | 													<Label className="text-sm text-gray-700 mb-1 block">Price</Label>
363 | 													<Input
364 | 														type="number"
365 | 														value={item.price}
366 | 														disabled
367 | 														className="bg-gray-100"
368 | 													/>
369 | 												</div>
370 | 												<div>
371 | 													<Label className="text-sm text-gray-700 mb-1 block">Total</Label>
372 | 													<Input
373 | 														value={item.total.toLocaleString("id-ID")}
374 | 														disabled
375 | 														className="bg-gray-100"
376 | 													/>
377 | 												</div>
378 | 											</div>
379 | 
380 | 											{/* Discount Each Item */}
381 | 											<div>
382 | 												<Label className="text-sm text-gray-700 mb-1 block">
383 | 													Discount (Optional)
384 | 												</Label>
385 | 												<div className="grid grid-cols-2 gap-2">
386 | 													<div>
387 | 														<Label className="text-xs text-gray-500">Percent (%)</Label>
388 | 														<Input
389 | 															type="number"
390 | 															placeholder="%"
391 | 															min={0}
392 | 															max={100}
393 | 															step="any"
394 | 															value={
395 | 																item.discountMode === "percent"
396 | 																	? item.discountInput
397 | 																	: calculateDiscountPercent(item)
398 | 															}
399 | 															onChange={(e) =>
400 | 																handleItemChange(index, "discountInput", e.target.value, "percent")
401 | 															}
402 | 														/>
403 | 													</div>
404 | 													<div>
405 | 														<Label className="text-xs text-gray-500">Amount (Rp)</Label>
406 | 														<Input
407 | 															type="number"
408 | 															placeholder="Rp"
409 | 															min={0}
410 | 															value={
411 | 																item.discountMode === "amount"
412 | 																	? item.discountInput
413 | 																	: item.discountAmount
414 | 															}
415 | 															onChange={(e) =>
416 | 																handleItemChange(index, "discountInput", e.target.value, "amount")
417 | 															}
418 | 														/>
419 | 													</div>
420 | 												</div>
421 | 											</div>
422 | 
423 | 											{/* Delete button */}
424 | 											<div className="flex justify-end">
425 | 												<Button
426 | 													type="button"
427 | 													variant="destructive"
428 | 													onClick={() => removeItem(index)}
429 | 													className="h-9 px-3"
430 | 												>
431 | 													<Trash2 className="w-4 h-4" />
432 | 												</Button>
433 | 											</div>
434 | 										</div>
435 | 									</div>
436 | 								))}
437 | 
438 | 								{/* Add Item Button */}
439 | 								<div className="md:col-span-3">
440 | 									<Button
441 | 										type="button"
442 | 										onClick={addItem}
443 | 										className="mt-2 bg-[#6D2315] hover:bg-[#591c10] text-white"
444 | 									>
445 | 										+ Add Item
446 | 									</Button>
447 | 								</div>
448 | 							</div>
449 | 
450 | 							{/* Shipping & Total */}
451 | 							<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
452 | 								{/* Discount General */}
453 | 								<div className="md:col-span-4">
454 | 									<div className="bg-[#fffaf0] border border-[#f4e3d3] rounded-md px-4 py-3 h-full">
455 | 										<Label className="block text-sm text-gray-700 mb-2">Discount (Optional)</Label>
456 | 										<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
457 | 											<div>
458 | 												<Label className="text-xs text-gray-500 mb-1 block">Percent (%)</Label>
459 | 												<Input
460 | 													type="number"
461 | 													min={0}
462 | 													max={100}
463 | 													step="any"
464 | 													value={
465 | 														discountMode === "percent"
466 | 															? discountInput
467 | 															: discountPercent.toFixed(2) || 0
468 | 													}
469 | 													onChange={(e) => {
470 | 														setDiscountMode("percent");
471 | 														setDiscountInput(e.target.value);
472 | 													}}
473 | 												/>
474 | 											</div>
475 | 
476 | 											<div>
477 | 												<Label className="text-xs text-gray-500 mb-1 block">Amount (Rp)</Label>
478 | 												<Input
479 | 													type="number"
480 | 													min={0}
481 | 													value={discountMode === "amount" ? discountInput : discountAmount}
482 | 													onChange={(e) => {
483 | 														setDiscountMode("amount");
484 | 														setDiscountInput(e.target.value);
485 | 													}}
486 | 												/>
487 | 											</div>
488 | 										</div>
489 | 									</div>
490 | 								</div>
491 | 
492 | 								<div className="md:col-span-8 hidden md:block"></div>
493 | 
494 | 								{/* Subtotal */}
495 | 								<div className="md:col-span-4">
496 | 									<Label className="py-2 block text-sm text-gray-700">Subtotal</Label>
497 | 									<Input
498 | 										value={subtotal.toLocaleString("id-ID")}
499 | 										disabled
500 | 										className="bg-gray-100"
501 | 									/>
502 | 								</div>
503 | 
504 | 								{/* Shipping */}
505 | 								<div className="md:col-span-4">
506 | 									<Label className="py-2 block text-sm text-gray-700">Shipping Price</Label>
507 | 									<Input
508 | 										type="number"
509 | 										value={shippingPrice}
510 | 										onChange={(e) => setShippingPrice(e.target.value)}
511 | 									/>
512 | 								</div>
513 | 
514 | 								{/* Total */}
515 | 								<div className="md:col-span-4">
516 | 									<Label className="py-2 block text-sm text-gray-700">Total Price</Label>
517 | 									<Input
518 | 										value={totalPrice.toLocaleString("id-ID")}
519 | 										disabled
520 | 										className="bg-gray-100"
521 | 									/>
522 | 								</div>
523 | 							</div>
524 | 
525 | 							<Button type="submit" className="w-full bg-[#6D2315] hover:bg-[#591c10] text-white">
526 | 								Save
527 | 							</Button>
528 | 						</form>
529 | 					</CardContent>
530 | 				</Card>
531 | 			</div>
532 | 		</section>
533 | 	);
534 | }
535 | 
```
Page 2/2FirstPrevNextLast