summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMiqdad <ahmadmiqdad27@gmail.com>2025-11-06 15:27:18 +0700
committerMiqdad <ahmadmiqdad27@gmail.com>2025-11-06 15:27:18 +0700
commit77a0a082f0a5e1977c85a651a078376eab0d6df1 (patch)
treedc1fc01ab55aa2f56b18dcab012f3ae19cb714aa /app
parent8a0e4716aa12ce335758c84f7fdcb4b47cd9c2db (diff)
<MIqdad> multiple image SJ
Diffstat (limited to 'app')
-rw-r--r--app/lib/camera/component/sjCamera.tsx89
-rw-r--r--app/lib/camera/hooks/useCameraStore.ts54
-rw-r--r--app/page.tsx99
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<HTMLInputElement | null>(null);
+
+ const readFilesAsDataURL = (files: FileList | null): Promise<string[]> =>
+ 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<HTMLInputElement>
) => {
- 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 (
- <>
- <div className="p-4 py-8 items-center border-2 rounded-md shadow-sm w-[49%] text-center">
- <input
- type="file"
- accept="image/*"
- onChange={handleSuratJalanCapture}
- className="hidden"
- id="suratJalanInput"
- />
- <label htmlFor="suratJalanInput" className="text-gray-600">
- <IconButton
- color="primary"
- aria-label="upload picture"
- component="span"
- >
- <PendingActions fontSize="large" />
- </IconButton>
- <br />
- Foto Surat Jalan
- </label>
- </div>
- </>
+ <div className="p-4 py-8 items-center border-2 rounded-md shadow-sm w-[49%] text-center">
+ <input
+ ref={fileRef}
+ id="suratJalanInput"
+ type="file"
+ accept="image/*"
+ capture="environment"
+ multiple
+ className="hidden"
+ onChange={handleSuratJalanCapture}
+ />
+ <label
+ htmlFor="suratJalanInput"
+ className="text-gray-600 cursor-pointer select-none"
+ >
+ <IconButton
+ color="primary"
+ aria-label="upload picture"
+ component="span"
+ >
+ <PendingActions fontSize="large" />
+ </IconButton>
+ <br />
+ Foto Surat Jalan
+ </label>
+ </div>
);
};
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<CameraStore>((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<boolean>(false); // start false biar nggak nge-flash
+ const [isLogin, setIsLogin] = useState<boolean>(false);
const [isDriver, setIsDriver] = useState<boolean>(false);
const [isDispatch, setIsDispatch] = useState<boolean>(false);
const [shippingMethod, setShippingMethod] = useState<ShipMethod>("");
@@ -42,12 +44,13 @@ export default function Home() {
setImageSj,
setImagePackage,
setImageDispatch,
+ removeSjImage,
} = useCameraStore();
const [isLoading, setIsLoading] = useState<boolean>(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<string, string> = {};
- 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() {
<div className="h-4" />
- <div className="flex justify-between">
+ <div className="flex justify-between gap-3">
{isDispatch ? (
<>
{showSjForDispatch && <SjCamera />}
@@ -251,21 +263,42 @@ export default function Home() {
<div className="h-2" />
- {/* Preview SJ */}
+ {/* Preview SJ (multi) + Remove per item */}
{showSjPreview && (
<>
<label className="block mt-2 text-sm font-medium text-gray-700 text-center">
Gambar Foto Surat Jalan
</label>
- <div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm">
- <Image
- src={imageSj!}
- alt="Captured"
- fill
- unoptimized
- className="p-2"
- style={{ objectFit: "cover" }}
- />
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
+ {sjArr.map((src, i) => (
+ <div
+ key={i}
+ className="relative w-full h-[180px] border-2 border-gray-200 rounded-sm"
+ >
+ <Image
+ src={src}
+ alt={`SJ ${i + 1}`}
+ fill
+ unoptimized
+ style={{ objectFit: "cover" }}
+ className="rounded-sm"
+ />
+ <div className="absolute top-1 right-1">
+ <Tooltip title="Remove">
+ <IconButton
+ size="small"
+ onClick={() => removeSjImage(i)}
+ sx={{
+ bgcolor: "rgba(255,255,255,0.85)",
+ "&:hover": { bgcolor: "rgba(255,255,255,1)" },
+ }}
+ >
+ <CloseIcon fontSize="small" />
+ </IconButton>
+ </Tooltip>
+ </div>
+ </div>
+ ))}
</div>
</>
)}
@@ -273,7 +306,7 @@ export default function Home() {
{/* Preview Penerima (hanya non-dispatch) */}
{!isDispatch && imagePackage && (
<>
- <label className="block mt-2 text-sm font-medium text-gray-700 text-center">
+ <label className="block mt-4 text-sm font-medium text-gray-700 text-center">
Gambar Foto Penerima
</label>
<div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm">
@@ -292,7 +325,7 @@ export default function Home() {
{/* Preview Dispatch */}
{showDispatchPreview && (
<>
- <label className="block mt-2 text-sm font-medium text-gray-700 text-center">
+ <label className="block mt-4 text-sm font-medium text-gray-700 text-center">
Gambar Foto Dispatch
</label>
<div className="relative w-full h-[300px] border-2 border-gray-200 p-2 rounded-sm">