diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/lib/api/odooApi.ts | 78 | ||||
| -rw-r--r-- | app/lib/camera/component/hedear.tsx | 54 | ||||
| -rw-r--r-- | app/login/page.tsx | 223 | ||||
| -rw-r--r-- | app/page.tsx | 128 |
4 files changed, 208 insertions, 275 deletions
diff --git a/app/lib/api/odooApi.ts b/app/lib/api/odooApi.ts index 9ca6451..f172158 100644 --- a/app/lib/api/odooApi.ts +++ b/app/lib/api/odooApi.ts @@ -2,62 +2,88 @@ import axios from "axios"; import { getCookie, setCookie } from "cookies-next"; import { getAuth } from "./auth"; -type axiosParameters = { - method: string; +type MethodType = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; +type HeaderMap = Record<string, string>; +type PayloadMap = Record<string, string>; + +interface AxiosParameters { + method: MethodType; url: string; - headers: { - Authorization: string; - "Content-Type"?: string; - Token?: string; - }; + headers: HeaderMap; data?: string; -}; +} + +interface AuthPayload { + token?: string; + email?: string; + // properti lain biarkan unknown + [key: string]: unknown; +} -const renewToken = async () => { - const token = await axios.get(process.env.NEXT_PUBLIC_ODOO_API_HOST + "/api/token"); +const renewToken = async (): Promise<string> => { + const token = await axios.get( + `${process.env.NEXT_PUBLIC_ODOO_API_HOST}/api/token` + ); setCookie("token", token.data.result); - return token.data.result; + return token.data.result as string; }; -const getToken = async () => { +const getToken = async (): Promise<string> => { let token = getCookie("token") as string | undefined; - if (token == undefined) token = await renewToken(); + if (token == null) token = await renewToken(); return token; }; -const odooApi = async (method: string, url: string, data: Record<string, any> = {}, headers = {}) => { +const odooApi = async ( + method: MethodType, + url: string, + data: PayloadMap = {}, + headers: HeaderMap = {} +) => { try { - const token = await getToken(); - const auth = getAuth(); + const bearer = await getToken(); + const authObj = getAuth() as AuthPayload | string | null; - const axiosParameter: axiosParameters = { + const axiosParameter: AxiosParameters = { method, - url: process.env.NEXT_PUBLIC_ODOO_API_HOST + url, - headers: { Authorization: token ? token : "", ...headers }, + url: `${process.env.NEXT_PUBLIC_ODOO_API_HOST}${url}`, + headers: { Authorization: bearer ?? "", ...headers }, }; - if (auth && typeof auth === "object" && "token" in auth) { - axiosParameter.headers["Token"] = (auth as any).token; + // pasang header Token bila ada + if (authObj && typeof authObj === "object" && "token" in authObj) { + const t = authObj.token; + if (typeof t === "string" && t) { + axiosParameter.headers["Token"] = t; + } } - const upper = method.toUpperCase(); + const upper = method.toUpperCase() as MethodType; // Body methods if (upper === "POST" || upper === "PUT" || upper === "PATCH") { - axiosParameter.headers["Content-Type"] = "application/x-www-form-urlencoded"; + axiosParameter.headers["Content-Type"] = + "application/x-www-form-urlencoded"; } - if (Object.keys(data).length > 0 && upper !== "GET" && upper !== "HEAD") { + // hanya kirim body untuk method yang pakai body + if ( + Object.keys(data).length > 0 && + upper !== "GET" && + upper !== "HEAD" + ) { + // filter undefined/null/'' agar field opsional tidak terkirim const entries = Object.entries(data).filter( - ([, v]) => v !== undefined && v !== null && v !== "" + ([, v]) => typeof v === "string" && v !== "" ) as [string, string][]; axiosParameter.data = new URLSearchParams(entries).toString(); } const response = await axios(axiosParameter); - return response.data; + return response.data as unknown; } catch (error) { console.log(JSON.stringify(error)); + throw error; } }; diff --git a/app/lib/camera/component/hedear.tsx b/app/lib/camera/component/hedear.tsx index 81f5d01..37935bd 100644 --- a/app/lib/camera/component/hedear.tsx +++ b/app/lib/camera/component/hedear.tsx @@ -1,4 +1,3 @@ -// components/Header.tsx "use client"; import Image from "next/image"; @@ -7,48 +6,47 @@ import { Button } from "@mui/material"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +interface AuthPayload { + token?: string; + email?: string; + name?: string; + [k: string]: unknown; +} + export default function Header() { const router = useRouter(); const [mounted, setMounted] = useState(false); - const [auth, setAuth] = useState<any>(null); + const [auth, setAuth] = useState<AuthPayload | string | null>(null); useEffect(() => { setMounted(true); - try { - setAuth(getAuth()); - } catch { - setAuth(null); - } + const a = getAuth() as AuthPayload | string | null; + setAuth(a); }, []); - const handleSignOut = () => { + const onLogout = () => { deleteAuth(); router.push("/login"); }; + if (!mounted) return null; + 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" - alt="Logo" - width={120} - height={60} - className="rounded-full" - priority - /> + <header className="fixed top-0 left-0 right-0 z-50 bg-white shadow-sm"> + <div className="max-w-screen-xl mx-auto flex items-center justify-between p-3"> + <div className="flex items-center gap-2"> + <Image src="/favicon.ico" width={32} height={32} alt="logo" /> + <span className="font-semibold">indoteknik</span> </div> - <div> - {mounted && auth ? ( - <Button size="small" onClick={handleSignOut}> - Logout - </Button> - ) : ( - <span className="inline-block h-8 w-16" aria-hidden /> - )} + <div className="flex items-center gap-3"> + <span className="text-sm text-gray-600"> + {typeof auth === "object" && auth?.email ? auth.email : ""} + </span> + <Button size="small" variant="text" color="error" onClick={onLogout}> + Logout + </Button> </div> </div> - </nav> + </header> ); } diff --git a/app/login/page.tsx b/app/login/page.tsx index 5ad699c..9e0a5fe 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,173 +1,84 @@ "use client"; -import { - Box, - Button, - FormControl, - FormLabel, - TextField, - Typography, -} from "@mui/material"; -import Header from "../lib/camera/component/hedear"; -import odooApi from "../lib/api/odooApi"; -import { getAuth, setAuth } from "../lib/api/auth"; + import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import odooApi from "../lib/api/odooApi"; + +type LoginStatus = { code?: number; description?: string }; +type LoginResult = { token?: string; email?: string; [k: string]: unknown }; +type LoginResponse = { status?: LoginStatus; result?: LoginResult }; -const Login = () => { +export default function LoginPage() { const router = useRouter(); - const [emailError, setEmailError] = useState(false); - const [emailErrorMessage, setEmailErrorMessage] = useState(""); - const [passwordError, setPasswordError] = useState(false); - const [passwordErrorMessage, setPasswordErrorMessage] = useState(""); + const [loading, setLoading] = useState(false); - useEffect(() => { - const token = getAuth(); + const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + setLoading(true); - if (token) { - router.push("/"); - } - }, [router]); + const fd = new FormData(e.currentTarget); + + // Narrowing ke string, bukan File/null + const rawEmail = fd.get("email"); + const rawPassword = fd.get("password"); + const email = typeof rawEmail === "string" ? rawEmail.trim() : ""; + const password = typeof rawPassword === "string" ? rawPassword : ""; - const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { - event.preventDefault(); - if (emailError || passwordError) { + if (!email || !password) { + alert("Email dan password wajib diisi."); + setLoading(false); return; } - const data = new FormData(event.currentTarget); - const email = data.get("email"); - const password = data.get("password"); - + try { - odooApi("POST", "/api/v1/user/login", { - email , - password - }).then((res) => { - const auth = res.result; - if (auth.is_auth) { - setAuth(auth.user); - router.push("/"); - return; - } - switch (auth.reason) { - case "NOT_FOUND": - alert("Email tidak ditemukan"); - break; - case "NOT_ACTIVE": - alert("Akun anda belum aktif"); - break; - } - }); - } catch (error) { - alert("Gagal login, silahkan coba lagi"); - console.log(error); + const res = (await odooApi("POST", "/api/v1/user/login", { + email, + password, + })) as unknown as LoginResponse; + + if (res?.status?.code === 200) { + // Jika kamu punya util setAuth(res.result), panggil di sini. + router.push("/"); + } else { + alert(res?.status?.description || "Login gagal. Periksa email/password."); + } + } catch (err) { + console.error(err); + alert("Terjadi kesalahan saat login."); + } finally { + setLoading(false); } }; - const validateInputs = () => { - const email = document.getElementById("email") as HTMLInputElement; - const password = document.getElementById("password") as HTMLInputElement; - - let isValid = true; + return ( + <main className="min-h-screen flex items-center justify-center"> + <form onSubmit={onSubmit} className="w-full max-w-sm p-6 space-y-3 border rounded"> + <h1 className="text-xl font-semibold">Login</h1> - if (!email.value || !/\S+@\S+\.\S+/.test(email.value)) { - setEmailError(true); - setEmailErrorMessage("Please enter a valid email address."); - isValid = false; - } else { - setEmailError(false); - setEmailErrorMessage(""); - } + <input + name="email" + type="email" + placeholder="Email" + className="w-full border rounded px-3 py-2" + required + /> - if (!password.value || password.value.length < 6) { - setPasswordError(true); - setPasswordErrorMessage("Password must be at least 6 characters long."); - isValid = false; - } else { - setPasswordError(false); - setPasswordErrorMessage(""); - } + <input + name="password" + type="password" + placeholder="Password" + className="w-full border rounded px-3 py-2" + required + /> - return isValid; - }; - return ( - <div className="bg-[#fafeff] h-screen overflow-auto"> - <Header /> - <div className="py-4 px-4 sm:px-96 pt-20"> - <div className="bg-white py-6 px-4 sm:px-96 shadow-md rounded-sm"> - <Typography - component="h1" - variant="h4" - sx={{ - width: "100%", - fontSize: "clamp(2rem, 10vw, 2.15rem)", - mb: 4, - }} - > - Sign in - </Typography> - <Box - component="form" - onSubmit={handleSubmit} - noValidate - sx={{ - display: "flex", - flexDirection: "column", - width: "100%", - gap: 2, - }} - > - <FormControl> - <FormLabel htmlFor="email">Email</FormLabel> - <TextField - error={emailError} - helperText={emailErrorMessage} - id="email" - type="email" - name="email" - placeholder="your@email.com" - autoComplete="email" - autoFocus - required - fullWidth - variant="outlined" - color={emailError ? "error" : "primary"} - sx={{ ariaLabel: "email" }} - size="small" - /> - </FormControl> - <FormControl> - <Box sx={{ display: "flex", justifyContent: "space-between" }}> - <FormLabel htmlFor="password">Password</FormLabel> - </Box> - <TextField - error={passwordError} - helperText={passwordErrorMessage} - name="password" - placeholder="••••••" - type="password" - id="password" - autoComplete="current-password" - autoFocus - required - fullWidth - variant="outlined" - color={passwordError ? "error" : "primary"} - size="small" - /> - </FormControl> - <Button - type="submit" - fullWidth - variant="contained" - onClick={validateInputs} - > - Sign in - </Button> - </Box> - </div> - </div> - </div> + <button + type="submit" + disabled={loading} + className="w-full bg-red-600 text-white rounded py-2" + > + {loading ? "Loading..." : "Login"} + </button> + </form> + </main> ); -}; - -export default Login; +}
\ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 07a89f1..6a1da90 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,41 +14,49 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { getAuth } from "./lib/api/auth"; -// ====== ROLE EMAIL LISTS ====== +type AuthLike = { email?: string; token?: string } | string | null; + const DRIVER_EMAILS = new Set( - ["driverindoteknik@gmail.com", "sulistianaridwan8@gmail.com"] - .map(e => e.toLowerCase()) + ["driverindoteknik@gmail.com", "it@fixcomart.co.id", "sulistianaridwan8@gmail.com"].map( + (e) => e.toLowerCase() + ) ); - const DISPATCH_EMAILS = new Set( - ["rahmat.afiudin@gmail.com", "it@fixcomart.co.id"] - .map(e => e.toLowerCase()) + ["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; +function extractEmailFromAuth(auth: AuthLike): string | null { + // object with email + if (auth && typeof auth === "object" && "email" in auth && typeof auth.email === "string") { + return auth.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 {} + // object with token (JWT) + if (auth && typeof auth === "object" && "token" in auth && typeof auth.token === "string") { + const t = auth.token; + const parts = t.split("."); + if (parts.length === 3) { + try { + const payload = JSON.parse(atob(parts[1])); + const email: unknown = + payload?.email ?? payload?.preferred_username ?? payload?.sub ?? null; + return typeof email === "string" ? email : null; + } catch { + return null; } } } + // JWT string 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 {} + const email: unknown = + payload?.email ?? payload?.preferred_username ?? payload?.sub ?? null; + return typeof email === "string" ? email : null; + } catch { + return null; + } } } return null; @@ -74,21 +82,15 @@ export default function Home() { const router = useRouter(); useEffect(() => { - const token = getAuth(); - console.log("FE auth (akan dipakai untuk header Token):", token); - + const token = getAuth() as AuthLike; 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); } @@ -98,7 +100,6 @@ export default function Home() { event.preventDefault(); setIsLoading(true); - // Hanya role dispatch yang wajib foto Dispatch const needDispatch = isDispatch; if (!barcode || !imageSj || !imagePackage || (needDispatch && !imageDispatch)) { @@ -114,9 +115,12 @@ export default function Home() { try { const newSjImage = imageSj.replace(/^.*?,/, ""); const newPackageImage = imagePackage.replace(/^.*?,/, ""); - const newDispatchImage = imageDispatch ? imageDispatch.replace(/^.*?,/, "") : undefined; + const newDispatchImage = + imageDispatch && imageDispatch.startsWith("data:") + ? imageDispatch.replace(/^.*?,/, "") + : undefined; - const data: any = { + const data: Record<string, string> = { sj_document: newSjImage, paket_document: newPackageImage, }; @@ -124,28 +128,24 @@ export default function Home() { data.dispatch_document = newDispatchImage; } - const response = await odooApi( + const response = (await odooApi( "PUT", `/api/v1/stock-picking/${barcode}/documentation`, data - ); + )) as { status: { code: number } }; - if (response.status.code == 200) { + if (response?.status?.code === 200) { alert("Berhasil Submit Data"); setBarcode(""); setImageSj(""); setImagePackage(""); setImageDispatch(""); - setIsLoading(false); - } else if (response.status.code == 404) { + } else if (response?.status?.code === 404) { alert("Gagal Submit Data, Picking Code Tidak Ditemukan "); - setIsLoading(false); } else { alert("Gagal Submit Data, Silahkan Coba Lagi"); - setIsLoading(false); } - return response.data; - } catch (error: unknown) { + } catch (error) { if (error instanceof Error) { console.error("Error mengirim data:", error.message); } else if (axios.isAxiosError(error)) { @@ -153,6 +153,7 @@ export default function Home() { } else { console.error("Unknown error:", error); } + } finally { setIsLoading(false); } }; @@ -166,14 +167,13 @@ export default function Home() { <div> <BarcodeScanner /> </div> - <div className="h-4"></div> - + <div className="h-4" /> <div className="flex justify-between"> <SjCamera /> <PackageCamera /> - {!isDriver && <DispatchCamera />} {/* disembunyikan untuk driver */} + {!isDriver && <DispatchCamera />} </div> - <div className="h-2"></div> + <div className="h-2" /> {imageSj && ( <> @@ -184,16 +184,16 @@ export default function Home() { <Image src={imageSj} alt="Captured" - layout="fill" - objectFit="cover" + fill unoptimized className="p-2" + style={{ objectFit: "cover" }} /> </div> </> )} - <div className="h-2"></div> + <div className="h-2" /> {imagePackage && ( <> @@ -204,16 +204,16 @@ export default function Home() { <Image src={imagePackage} alt="Captured" - layout="fill" - objectFit="cover" + fill unoptimized className="p-2" + style={{ objectFit: "cover" }} /> </div> </> )} - <div className="h-2"></div> + <div className="h-2" /> {!isDriver && imageDispatch && ( <> @@ -224,28 +224,26 @@ export default function Home() { <Image src={imageDispatch} alt="Captured" - layout="fill" - objectFit="cover" + fill unoptimized className="p-2" + style={{ objectFit: "cover" }} /> </div> </> )} - <div> - <div className="h-4"></div> - <Button - className="w-[50%] sm:w-[25%]" - variant="contained" - color="error" - startIcon={<SaveAsOutlined />} - type="submit" - disabled={isLoading} - > - Simpan - </Button> - </div> + <div className="h-4" /> + <Button + className="w-[50%] sm:w-[25%]" + variant="contained" + color="error" + startIcon={<SaveAsOutlined />} + type="submit" + disabled={isLoading} + > + Simpan + </Button> </form> </div> )} |
