summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiqdad <ahmadmiqdad27@gmail.com>2025-09-20 10:32:53 +0700
committerMiqdad <ahmadmiqdad27@gmail.com>2025-09-20 10:32:53 +0700
commitbf8b6aacbbfef3df4b56be5e63e8f983d71f9c7c (patch)
tree2f45681975767399fe2ca855077e6dc928f9931c
parent5c3d6cfd6f68b6eb7192aba47463bd0541bfbf48 (diff)
<Miqdad> fix compile erroradd_role
-rw-r--r--app/lib/api/odooApi.ts78
-rw-r--r--app/lib/camera/component/hedear.tsx54
-rw-r--r--app/login/page.tsx223
-rw-r--r--app/page.tsx128
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>
)}