From 77a0a082f0a5e1977c85a651a078376eab0d6df1 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 6 Nov 2025 15:27:18 +0700 Subject: multiple image SJ --- app/lib/camera/component/sjCamera.tsx | 89 +++++++++++++++++++----------- app/lib/camera/hooks/useCameraStore.ts | 54 +++++++++++-------- app/page.tsx | 99 ++++++++++++++++++++++------------ 3 files changed, 157 insertions(+), 85 deletions(-) diff --git a/app/lib/camera/component/sjCamera.tsx b/app/lib/camera/component/sjCamera.tsx index 9dbe2dc..ea5c5e2 100644 --- a/app/lib/camera/component/sjCamera.tsx +++ b/app/lib/camera/component/sjCamera.tsx @@ -1,46 +1,73 @@ -import React from "react"; +import React, { useRef } from "react"; import useCameraStore from "../hooks/useCameraStore"; import { IconButton } from "@mui/material"; import { PendingActions } from "@mui/icons-material"; const SjCamera: React.FC = () => { const { setImageSj } = useCameraStore(); - const handleSuratJalanCapture = ( + const fileRef = useRef(null); + + const readFilesAsDataURL = (files: FileList | null): Promise => + new Promise((resolve) => { + if (!files || files.length === 0) return resolve([]); + const list = Array.from(files); + const out: string[] = []; + let done = 0; + list.forEach((f) => { + const fr = new FileReader(); + fr.onload = (e) => { + const dataUrl = (e.target?.result as string) || ""; + if (dataUrl) out.push(dataUrl); + done += 1; + if (done === list.length) resolve(out); + }; + fr.onerror = () => { + done += 1; + if (done === list.length) resolve(out); + }; + fr.readAsDataURL(f); + }); + }); + + const handleSuratJalanCapture = async ( event: React.ChangeEvent ) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onloadend = () => { - setImageSj(reader.result as string); - }; - reader.readAsDataURL(file); + const imgs = await readFilesAsDataURL(event.target.files); + if (imgs.length > 0) { + // APPEND: panggil setImageSj untuk tiap foto + imgs.forEach((img) => setImageSj(img)); } + // reset supaya bisa pilih file yang sama lagi + if (fileRef.current) fileRef.current.value = ""; }; return ( - <> -
- - -
- +
+ + +
); }; diff --git a/app/lib/camera/hooks/useCameraStore.ts b/app/lib/camera/hooks/useCameraStore.ts index ad83074..874c627 100644 --- a/app/lib/camera/hooks/useCameraStore.ts +++ b/app/lib/camera/hooks/useCameraStore.ts @@ -1,26 +1,38 @@ -// store/useCameraStore.ts -import { create } from 'zustand' +// lib/camera/hooks/useCameraStore.ts +import { create } from "zustand"; interface CameraStore { - barcode: string | null - setBarcode: (barcode: string) => void - imageSj: string | null - setImageSj: (image: string) => void - imagePackage: string | null - setImagePackage: (image: string) => void - imageDispatch: string | null - setImageDispatch: (image: string) => void + barcode: string; + setBarcode: (barcode: string) => void; + + imageSj: string[]; // dataURL + setImageSj: (imgOrArr: string | string[]) => void; + removeSjImage: (idx: number) => void; + + imagePackage: string | null; + setImagePackage: (image: string | null) => void; + + imageDispatch: string | null; + setImageDispatch: (image: string | null) => void; } const useCameraStore = create((set) => ({ - barcode: '', - setBarcode: (barcode: string) => set({ barcode: barcode }), - imageSj: '', - setImageSj: (image: string) => set({ imageSj: image }), - imagePackage: '', - setImagePackage: (image: string) => set({ imagePackage: image }), - imageDispatch: '', - setImageDispatch: (image: string) => set({ imageDispatch: image }), -})) - -export default useCameraStore \ No newline at end of file + barcode: "", + setBarcode: (barcode) => set({ barcode }), + + imageSj: [], + setImageSj: (imgOrArr) => + set((s) => ({ + imageSj: Array.isArray(imgOrArr) ? imgOrArr : [...s.imageSj, imgOrArr], + })), + removeSjImage: (idx) => + set((s) => ({ imageSj: s.imageSj.filter((_, i) => i !== idx) })), + + imagePackage: "", + setImagePackage: (image) => set({ imagePackage: image }), + + imageDispatch: "", + setImageDispatch: (image) => set({ imageDispatch: image }), +})); + +export default useCameraStore; diff --git a/app/page.tsx b/app/page.tsx index ae0e4d8..35609a6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,8 +13,10 @@ import { MenuItem, Select, FormHelperText, + IconButton, + Tooltip, } from "@mui/material"; -import { SaveAsOutlined } from "@mui/icons-material"; +import { SaveAsOutlined, Close as CloseIcon } from "@mui/icons-material"; import axios from "axios"; import odooApi from "./lib/api/odooApi"; import { useEffect, useState } from "react"; @@ -27,7 +29,7 @@ type Role = "driver" | "dispatch"; type ShipMethod = "" | "self_pickup" | "indoteknik_delivery" | "ekspedisi"; export default function Home() { - const [isLogin, setIsLogin] = useState(false); // start false biar nggak nge-flash + const [isLogin, setIsLogin] = useState(false); const [isDriver, setIsDriver] = useState(false); const [isDispatch, setIsDispatch] = useState(false); const [shippingMethod, setShippingMethod] = useState(""); @@ -42,12 +44,13 @@ export default function Home() { setImageSj, setImagePackage, setImageDispatch, + removeSjImage, } = useCameraStore(); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); - // Single effect: auth gate + set role + // Auth gate + role dari cookie useEffect(() => { const auth = getAuth(); if (!auth) { @@ -73,7 +76,6 @@ export default function Home() { return; } - // Dispatch: shipping method wajib if (isDispatch) setShipTouched(true); if (isDispatch && !shippingMethod) { alert("Shipping Method wajib dipilih."); @@ -81,17 +83,20 @@ export default function Home() { return; } - // Validasi foto sesuai role & shipping method + const sjArr: string[] = Array.isArray(imageSj) + ? imageSj + : imageSj + ? [imageSj as unknown as string] + : []; + if (isDispatch) { if (!imageDispatch) { alert("Foto Dispatch Wajib Diisi"); setIsLoading(false); return; } - // SJ opsional untuk self_pickup & ekspedisi } else { - // Driver: SJ & Penerima wajib - if (!imageSj || !imagePackage) { + if (sjArr.length < 1 || !imagePackage) { alert("Barcode, Foto SJ, dan Foto Penerima harus tersedia."); setIsLoading(false); return; @@ -99,10 +104,13 @@ export default function Home() { } try { - const newSjImage = imageSj ? imageSj.replace(/^.*?,/, "") : undefined; + const latestSj = sjArr.at(-1) || null; + const newSjImage = latestSj ? latestSj.replace(/^.*?,/, "") : undefined; + const newPackageImage = imagePackage ? imagePackage.replace(/^.*?,/, "") : undefined; + const newDispatchImage = imageDispatch && imageDispatch.startsWith("data:") ? imageDispatch.replace(/^.*?,/, "") @@ -114,14 +122,11 @@ export default function Home() { const submittedDispatch = !!newDispatchImage && !isDriver; const data: Record = {}; - if (submittedSj) data.sj_document = newSjImage!; + if (submittedSj) data.sj_documentations = newSjImage; // plural sesuai API if (submittedPackage) data.paket_document = newPackageImage!; if (submittedDispatch) data.dispatch_document = newDispatchImage!; if (isDispatch && shippingMethod) data.shipping_method = shippingMethod; - - if (shippingMethod === "self_pickup") { - data.self_pu = "true"; - } + if (shippingMethod === "self_pickup") data.self_pu = "true"; const response = (await odooApi( "PUT", @@ -132,8 +137,11 @@ export default function Home() { if (response?.status?.code === 200) { alert("Berhasil Submit Data"); setBarcode(""); - // Bersihkan HANYA yang dikirim - if (submittedSj) setImageSj(""); + + if (submittedSj) { + const next = sjArr.slice(0, -1); + setImageSj(next); + } if (submittedPackage) setImagePackage(""); if (submittedDispatch) setImageDispatch(""); } else if (response?.status?.code === 404) { @@ -155,15 +163,19 @@ export default function Home() { }; // === UI helpers === + const sjArr: string[] = Array.isArray(imageSj) + ? imageSj + : imageSj + ? [imageSj as unknown as string] + : []; + // dispatch: SJ hanya utk self_pickup & ekspedisi const showSjForDispatch = isDispatch && (shippingMethod === "self_pickup" || shippingMethod === "ekspedisi"); - // dispatch: kamera dispatch tampil utk semua method (asal sudah dipilih) const showDispatchForDispatch = isDispatch && shippingMethod !== ""; - // preview SJ: sembunyikan kalau dispatch belum pilih method - const showSjPreview = !!imageSj && (!isDispatch || showSjForDispatch); - // preview Dispatch: sembunyikan kalau dispatch belum pilih method + + const showSjPreview = sjArr.length > 0 && (!isDispatch || showSjForDispatch); const showDispatchPreview = !!imageDispatch && (!isDispatch || showDispatchForDispatch); @@ -234,7 +246,7 @@ export default function Home() {
-
+
{isDispatch ? ( <> {showSjForDispatch && } @@ -251,21 +263,42 @@ export default function Home() {
- {/* Preview SJ */} + {/* Preview SJ (multi) + Remove per item */} {showSjPreview && ( <> -
- Captured +
+ {sjArr.map((src, i) => ( +
+ {`SJ +
+ + removeSjImage(i)} + sx={{ + bgcolor: "rgba(255,255,255,0.85)", + "&:hover": { bgcolor: "rgba(255,255,255,1)" }, + }} + > + + + +
+
+ ))}
)} @@ -273,7 +306,7 @@ export default function Home() { {/* Preview Penerima (hanya non-dispatch) */} {!isDispatch && imagePackage && ( <> -