diff options
| -rw-r--r-- | app/lib/api/odooApi.ts | 92 | ||||
| -rw-r--r-- | app/lib/camera/component/dispatchCamera.tsx | 43 | ||||
| -rw-r--r-- | app/lib/camera/component/hedear.tsx | 37 | ||||
| -rw-r--r-- | app/lib/camera/hooks/useCameraStore.ts | 10 | ||||
| -rw-r--r-- | app/page.tsx | 112 |
5 files changed, 229 insertions, 65 deletions
diff --git a/app/lib/api/odooApi.ts b/app/lib/api/odooApi.ts index c2c9d82..9ca6451 100644 --- a/app/lib/api/odooApi.ts +++ b/app/lib/api/odooApi.ts @@ -1,56 +1,64 @@ -import axios from "axios" +import axios from "axios"; import { getCookie, setCookie } from "cookies-next"; import { getAuth } from "./auth"; type axiosParameters = { - method : string, - url : string, - headers : { - Authorization : string, - 'Content-Type'? : string, - Token? : string - }, - data ?: string -} + method: string; + url: string; + headers: { + Authorization: string; + "Content-Type"?: string; + Token?: string; + }; + data?: string; +}; const renewToken = async () => { - const token = await axios.get(process.env.NEXT_PUBLIC_ODOO_API_HOST + '/api/token') - setCookie('token', token.data.result) - return token.data.result + const token = await axios.get(process.env.NEXT_PUBLIC_ODOO_API_HOST + "/api/token"); + setCookie("token", token.data.result); + return token.data.result; }; const getToken = async () => { - let token = getCookie('token') - if (token == undefined) token = await renewToken() - return token + let token = getCookie("token") as string | undefined; + if (token == undefined) token = await renewToken(); + return token; }; -const odooApi = async (method : string, url : string, data = {}, headers = {}) => { - try { - const token = await getToken() - const auth = getAuth(); - const axiosParameter : axiosParameters = { - method, - url: process.env.NEXT_PUBLIC_ODOO_API_HOST + url, - headers: { Authorization: token ? token : '', ...headers }, - }; - console.log('ini adalah tipe',axiosParameter) - if (auth && typeof auth === 'object' && 'token' in auth) { - axiosParameter.headers['Token'] = auth.token; - } - if (method.toUpperCase() == 'POST') - axiosParameter.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - if (Object.keys(data).length > 0) - axiosParameter.data = new URLSearchParams( - Object.entries(data) - ).toString(); - const response = await axios(axiosParameter); - return response.data; - } catch (error) { - console.log( JSON.stringify(error)); +const odooApi = async (method: string, url: string, data: Record<string, any> = {}, headers = {}) => { + try { + const token = await getToken(); + const auth = getAuth(); + + const axiosParameter: axiosParameters = { + method, + url: process.env.NEXT_PUBLIC_ODOO_API_HOST + url, + headers: { Authorization: token ? token : "", ...headers }, + }; + + if (auth && typeof auth === "object" && "token" in auth) { + axiosParameter.headers["Token"] = (auth as any).token; } -} + const upper = method.toUpperCase(); + + // Body methods + if (upper === "POST" || upper === "PUT" || upper === "PATCH") { + axiosParameter.headers["Content-Type"] = "application/x-www-form-urlencoded"; + } + + if (Object.keys(data).length > 0 && upper !== "GET" && upper !== "HEAD") { + const entries = Object.entries(data).filter( + ([, v]) => v !== undefined && v !== null && v !== "" + ) as [string, string][]; + axiosParameter.data = new URLSearchParams(entries).toString(); + } + + const response = await axios(axiosParameter); + return response.data; + } catch (error) { + console.log(JSON.stringify(error)); + } +}; -export default odooApi
\ No newline at end of file +export default odooApi; diff --git a/app/lib/camera/component/dispatchCamera.tsx b/app/lib/camera/component/dispatchCamera.tsx new file mode 100644 index 0000000..2591413 --- /dev/null +++ b/app/lib/camera/component/dispatchCamera.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import useCameraStore from "../hooks/useCameraStore"; +import { IconButton } from "@mui/material"; +import { LocalShipping } from "@mui/icons-material"; + +const DispatchCamera: React.FC = () => { + const { setImageDispatch } = useCameraStore(); + const handleCapture = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setImageDispatch(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + return ( + <div className="px-4 py-8 items-center border-2 rounded-md shadow-sm w-[49%] text-center "> + <input + type="file" + accept="image/*" + onChange={handleCapture} + className="hidden" + id="dispatchCameraInput" + /> + <label htmlFor="dispatchCameraInput" className="text-gray-600"> + <IconButton + color="primary" + aria-label="upload picture" + component="span" + > + <LocalShipping fontSize="large" /> + </IconButton> + <br /> + Foto Dispatch + </label> + </div> + ); +}; + +export default DispatchCamera; diff --git a/app/lib/camera/component/hedear.tsx b/app/lib/camera/component/hedear.tsx index 2a8bfc6..81f5d01 100644 --- a/app/lib/camera/component/hedear.tsx +++ b/app/lib/camera/component/hedear.tsx @@ -1,36 +1,53 @@ // components/Header.tsx +"use client"; + import Image from "next/image"; import { deleteAuth, getAuth } from "../../api/auth"; import { Button } from "@mui/material"; import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; export default function Header() { - const auth = getAuth(); - const route = useRouter(); + const router = useRouter(); + const [mounted, setMounted] = useState(false); + const [auth, setAuth] = useState<any>(null); + + useEffect(() => { + setMounted(true); + try { + setAuth(getAuth()); + } catch { + setAuth(null); + } + }, []); - const handleSigOut = () => { + const handleSignOut = () => { deleteAuth(); - route.push('/login'); + router.push("/login"); }; + return ( <nav className="fixed top-0 left-0 w-full bg-white border-b-2 border-red-500 py-4 px-4 sm:px-96 z-50 shadow-md"> <div className="flex justify-between items-center"> <div className="flex items-center"> <Image - src="/images/indoteknik-logo.png" // Ganti dengan path logo Anda + src="/images/indoteknik-logo.png" alt="Logo" width={120} height={60} className="rounded-full" + priority /> </div> - {auth && ( - <div> - <Button size="small" onClick={() => handleSigOut()}> + <div> + {mounted && auth ? ( + <Button size="small" onClick={handleSignOut}> Logout </Button> - </div> - )} + ) : ( + <span className="inline-block h-8 w-16" aria-hidden /> + )} + </div> </div> </nav> ); diff --git a/app/lib/camera/hooks/useCameraStore.ts b/app/lib/camera/hooks/useCameraStore.ts index c922d1c..ad83074 100644 --- a/app/lib/camera/hooks/useCameraStore.ts +++ b/app/lib/camera/hooks/useCameraStore.ts @@ -8,15 +8,19 @@ interface CameraStore { setImageSj: (image: string) => void imagePackage: string | null setImagePackage: (image: string) => void + imageDispatch: string | null + setImageDispatch: (image: string) => void } const useCameraStore = create<CameraStore>((set) => ({ - barcode: null, + barcode: '', setBarcode: (barcode: string) => set({ barcode: barcode }), - imageSj: null, + imageSj: '', setImageSj: (image: string) => set({ imageSj: image }), - imagePackage: null, + imagePackage: '', setImagePackage: (image: string) => set({ imagePackage: image }), + imageDispatch: '', + setImageDispatch: (image: string) => set({ imageDispatch: image }), })) export default useCameraStore
\ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index f12f746..07a89f1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import PackageCamera from "./lib/camera/component/pakageCamera"; import BarcodeScanner from "./lib/camera/component/scannerBarcode"; import SjCamera from "./lib/camera/component/sjCamera"; +import DispatchCamera from "./lib/camera/component/dispatchCamera"; import useCameraStore from "./lib/camera/hooks/useCameraStore"; import Header from "./lib/camera/component/hedear"; import { Button } from "@mui/material"; @@ -13,27 +14,83 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { getAuth } from "./lib/api/auth"; +// ====== ROLE EMAIL LISTS ====== +const DRIVER_EMAILS = new Set( + ["driverindoteknik@gmail.com", "sulistianaridwan8@gmail.com"] + .map(e => e.toLowerCase()) +); + +const DISPATCH_EMAILS = new Set( + ["rahmat.afiudin@gmail.com", "it@fixcomart.co.id"] + .map(e => e.toLowerCase()) +); + +function extractEmailFromAuth(auth: unknown): string | null { + if (auth && typeof auth === "object" && "email" in (auth as any)) { + const email = (auth as any).email; + if (typeof email === "string") return email; + } + if (auth && typeof auth === "object" && "token" in (auth as any)) { + const t = (auth as any).token; + if (typeof t === "string") { + const parts = t.split("."); + if (parts.length === 3) { + try { + const payload = JSON.parse(atob(parts[1])); + return payload?.email ?? payload?.preferred_username ?? payload?.sub ?? null; + } catch {} + } + } + } + if (typeof auth === "string") { + const parts = auth.split("."); + if (parts.length === 3) { + try { + const payload = JSON.parse(atob(parts[1])); + return payload?.email ?? payload?.preferred_username ?? payload?.sub ?? null; + } catch {} + } + } + return null; +} + export default function Home() { const [isLogin, setIsLogin] = useState<boolean>(true); + const [isDriver, setIsDriver] = useState<boolean>(false); + const [isDispatch, setIsDispatch] = useState<boolean>(false); + const { barcode, imageSj, imagePackage, + imageDispatch, setBarcode, setImageSj, setImagePackage, + setImageDispatch, } = useCameraStore(); - const [isLoading, setIsLoading] = useState<boolean>(false); + const [isLoading, setIsLoading] = useState<boolean>(false); const router = useRouter(); useEffect(() => { const token = getAuth(); + console.log("FE auth (akan dipakai untuk header Token):", token); if (!token) { router.push("/login"); } else { setIsLogin(true); + + const email = extractEmailFromAuth(token); + const lower = (email ?? "").toLowerCase(); + + // PRIORITAS: dispatch > driver + const dispatchFlag = DISPATCH_EMAILS.has(lower); + const driverFlag = DRIVER_EMAILS.has(lower) && !dispatchFlag; + + setIsDispatch(dispatchFlag); + setIsDriver(driverFlag); } }, [router]); @@ -41,8 +98,15 @@ export default function Home() { event.preventDefault(); setIsLoading(true); - if (!barcode || !imageSj || !imagePackage) { - alert("Barcode dan gambar harus tersedia."); + // Hanya role dispatch yang wajib foto Dispatch + const needDispatch = isDispatch; + + if (!barcode || !imageSj || !imagePackage || (needDispatch && !imageDispatch)) { + alert( + needDispatch + ? "Barcode, Foto SJ, Foto Penerima, dan Foto Dispatch harus tersedia." + : "Barcode, Foto SJ, dan Foto Penerima harus tersedia." + ); setIsLoading(false); return; } @@ -50,28 +114,34 @@ export default function Home() { try { const newSjImage = imageSj.replace(/^.*?,/, ""); const newPackageImage = imagePackage.replace(/^.*?,/, ""); + const newDispatchImage = imageDispatch ? imageDispatch.replace(/^.*?,/, "") : undefined; - const data = { - sj_document: newSjImage, // Kirim base64 lengkap dengan prefix - paket_document: newPackageImage, // Kirim base64 lengkap dengan prefix + const data: any = { + sj_document: newSjImage, + paket_document: newPackageImage, }; + if (!isDriver && newDispatchImage) { + data.dispatch_document = newDispatchImage; + } const response = await odooApi( "PUT", `/api/v1/stock-picking/${barcode}/documentation`, data ); + if (response.status.code == 200) { alert("Berhasil Submit Data"); setBarcode(""); setImageSj(""); setImagePackage(""); + setImageDispatch(""); setIsLoading(false); - }else if(response.status.code == 404){ - alert("Gagal Submit Data, Picking Code Tidak Ditemukan " ); + } else if (response.status.code == 404) { + alert("Gagal Submit Data, Picking Code Tidak Ditemukan "); setIsLoading(false); - }else{ - alert("Gagal Submit Data, Silahkan Coba Lagi" ); + } else { + alert("Gagal Submit Data, Silahkan Coba Lagi"); setIsLoading(false); } return response.data; @@ -101,6 +171,7 @@ export default function Home() { <div className="flex justify-between"> <SjCamera /> <PackageCamera /> + {!isDriver && <DispatchCamera />} {/* disembunyikan untuk driver */} </div> <div className="h-2"></div> @@ -141,6 +212,27 @@ export default function Home() { </div> </> )} + + <div className="h-2"></div> + + {!isDriver && imageDispatch && ( + <> + <label className="block mt-2 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"> + <Image + src={imageDispatch} + alt="Captured" + layout="fill" + objectFit="cover" + unoptimized + className="p-2" + /> + </div> + </> + )} + <div> <div className="h-4"></div> <Button |
