From ba03898e8b574fe250e85530c9b622f4316a996b Mon Sep 17 00:00:00 2001 From: Sabda Yagra Date: Thu, 18 Dec 2025 10:00:24 +0700 Subject: [PATCH] fix: adjust register to polri member --- components/auth/identity-form.tsx | 208 ++++++++++++++++++++----- components/auth/profile-form.tsx | 243 ++++++++++++++++++++++++++---- types/registration.ts | 2 +- 3 files changed, 389 insertions(+), 64 deletions(-) diff --git a/components/auth/identity-form.tsx b/components/auth/identity-form.tsx index a6493082..e8898ac4 100644 --- a/components/auth/identity-form.tsx +++ b/components/auth/identity-form.tsx @@ -1,6 +1,5 @@ "use client"; -import React, { useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Button } from "@/components/ui/button"; @@ -8,7 +7,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useTranslations } from "next-intl"; import { FormField } from "@/components/auth/form-field"; -import { +import { IdentityFormProps, JournalistRegistrationData, PersonnelRegistrationData, @@ -16,10 +15,11 @@ import { UserCategory, journalistRegistrationSchema, personnelRegistrationSchema, - generalRegistrationSchema + generalRegistrationSchema, } from "@/types/registration"; import { useUserDataValidation } from "@/hooks/use-registration"; import { ASSOCIATIONS } from "@/lib/registration-utils"; +import React, { useState, useRef } from "react"; export const IdentityForm: React.FC = ({ category, @@ -28,11 +28,48 @@ export const IdentityForm: React.FC = ({ className, }) => { const t = useTranslations("LandingPage"); - const { validateJournalistData, validatePersonnelData, loading: validationLoading } = useUserDataValidation(); - + const { + validateJournalistData, + validatePersonnelData, + loading: validationLoading, + } = useUserDataValidation(); + const [memberIdentity, setMemberIdentity] = useState(""); const [memberIdentityError, setMemberIdentityError] = useState(""); + const [personelName, setPersonelName] = useState(""); + const [loadingPersonel, setLoadingPersonel] = useState(false); + + const fetchPersonelByNRP = async (nrp: string) => { + setLoadingPersonel(true); + try { + const res = await fetch( + `https://mediahub.polri.go.id/api/v2/public/users/search-personil?nrp=${nrp}&timemilis=${Date.now()}` + ); + + if (!res.ok) { + throw new Error("Personel not found"); + } + + const json = await res.json(); + + // asumsi responseData ada di json.data + const name = json?.data?.nama; + + if (!name) { + throw new Error("Nama personel tidak ditemukan"); + } + + setPersonelName(name); + setValue("policeNumber" as keyof PersonnelRegistrationData, nrp); + } catch (err: any) { + setPersonelName(""); + setMemberIdentityError(err.message || "NRP tidak valid"); + } finally { + setLoadingPersonel(false); + } + }; + // Determine which schema to use based on category const getSchema = () => { switch (category) { @@ -51,58 +88,133 @@ export const IdentityForm: React.FC = ({ formState: { errors, isSubmitting }, setValue, watch, - } = useForm({ + } = useForm< + | JournalistRegistrationData + | PersonnelRegistrationData + | GeneralRegistrationData + >({ resolver: zodResolver(getSchema()), mode: "onChange", }); const watchedEmail = watch("email"); - const handleMemberIdentityChange = async (value: string) => { + // const handleMemberIdentityChange = async (value: string) => { + // setMemberIdentity(value); + // setMemberIdentityError(""); + + // if (!value.trim()) { + // return; + // } + + // try { + // if (category === "6") { + // await validateJournalistData(value); + // setValue( + // "journalistCertificate" as keyof JournalistRegistrationData, + // value + // ); + // } else if (category === "7") { + // await validatePersonnelData(value); + // setValue("policeNumber" as keyof PersonnelRegistrationData, value); + // } + // } catch (error: any) { + // setMemberIdentityError(error.message || "Invalid identity number"); + // } + // }; + + const handleMemberIdentityChange = (value: string) => { setMemberIdentity(value); setMemberIdentityError(""); + // reset nama personel jika input kosong if (!value.trim()) { + setPersonelName(""); return; } - try { - if (category === "6") { - await validateJournalistData(value); - setValue("journalistCertificate" as keyof JournalistRegistrationData, value); - } else if (category === "7") { - await validatePersonnelData(value); - setValue("policeNumber" as keyof PersonnelRegistrationData, value); - } - } catch (error: any) { - setMemberIdentityError(error.message || "Invalid identity number"); + // hanya debounce untuk PERSONNEL (category 7) + if (category !== "7") return; + + // clear timer sebelumnya + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); } + + // set timer baru (3 detik) + debounceTimerRef.current = setTimeout(async () => { + try { + await fetchPersonelByNRP(value); + } catch (error: any) { + setMemberIdentityError(error.message || "NRP tidak valid"); + } + }, 2000); }; - const onSubmit = async (data: JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData) => { + const onSubmit = async ( + data: + | JournalistRegistrationData + | PersonnelRegistrationData + | GeneralRegistrationData + ) => { try { - // Additional validation for member identity - if ((category === "6" || category === "7") && !memberIdentity.trim()) { - setMemberIdentityError("Identity number is required"); - return; + if (category === "7") { + if (!memberIdentity.trim()) { + setMemberIdentityError("NRP wajib diisi"); + return; + } + + if (!personelName) { + setMemberIdentityError("Data personel belum valid"); + return; + } } - if (memberIdentityError) { - onError?.(memberIdentityError); - return; - } + const payload = { + ...data, + nrp: memberIdentity, + personelName, + }; - onSuccess?.(data); + // simpan sementara untuk step OTP & next page + sessionStorage.setItem("registration_identity", JSON.stringify(payload)); + + onSuccess?.(payload); } catch (error: any) { onError?.(error.message || "Form submission failed"); } }; + // const onSubmit = async ( + // data: + // | JournalistRegistrationData + // | PersonnelRegistrationData + // | GeneralRegistrationData + // ) => { + // try { + // // Additional validation for member identity + // if ((category === "6" || category === "7") && !memberIdentity.trim()) { + // setMemberIdentityError("Identity number is required"); + // return; + // } + + // if (memberIdentityError) { + // onError?.(memberIdentityError); + // return; + // } + + // onSuccess?.(data); + // } catch (error: any) { + // onError?.(error.message || "Form submission failed"); + // } + // }; + const renderJournalistFields = () => ( <>
handleMemberIdentityChange(e.target.value)} @@ -142,10 +259,13 @@ export const IdentityForm: React.FC = ({ ); + const debounceTimerRef = useRef(null); + const renderPersonnelFields = () => (
= ({ {memberIdentityError && (

{memberIdentityError}

)} + + {personelName && ( +
+ + +
+ )}
); @@ -177,19 +308,26 @@ export const IdentityForm: React.FC = ({ {errors.email && ( -

{errors.email.message}

+

+ {errors.email.message} +

)}
{/* Terms and conditions */}

- {t("byRegis", { defaultValue: "By registering, you agree to our" })}
{" "} + {t("byRegis", { + defaultValue: "By registering, you agree to our", + })}{" "} +
{" "} {t("terms", { defaultValue: "Terms of Service" })} {" "} @@ -221,4 +359,4 @@ export const IdentityForm: React.FC = ({

); -}; \ No newline at end of file +}; diff --git a/components/auth/profile-form.tsx b/components/auth/profile-form.tsx index 68c83265..8d656a9b 100644 --- a/components/auth/profile-form.tsx +++ b/components/auth/profile-form.tsx @@ -26,6 +26,11 @@ const PasswordChecklist = dynamic(() => import("react-password-checklist"), { ssr: false, }); +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; + +const MySwal = withReactContent(Swal); + export const ProfileForm: React.FC = ({ userData, category, @@ -60,6 +65,15 @@ export const ProfileForm: React.FC = ({ const [passwordConf, setPasswordConf] = useState(""); const [showPassword, setShowPassword] = useState(false); const [showPasswordConf, setShowPasswordConf] = useState(false); + const [usernameTouched, setUsernameTouched] = useState(false); + + const [otpIdentity, setOtpIdentity] = useState<{ + email: string; + nrp?: string; + personelName?: string; + }>({ + email: "", + }); const { register, @@ -70,9 +84,38 @@ export const ProfileForm: React.FC = ({ } = useForm({ resolver: zodResolver(registrationSchema), mode: "onChange", - defaultValues: { email: userData, }, + defaultValues: { email: userData }, }); + React.useEffect(() => { + const stored = sessionStorage.getItem("registration_identity"); + + if (stored) { + const parsed = JSON.parse(stored); + + setOtpIdentity({ + email: parsed.email, + nrp: parsed.nrp, + personelName: parsed.personelName, + }); + + // default nama dari API + if (parsed.personelName) { + setValue("firstName", parsed.personelName); + + const autoUsername = parsed.personelName + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); + + setValue("username", autoUsername); + } + + setValue("email", parsed.email); + } + }, [setValue]); + const watchedPassword = watch("password"); const handleProvinceChange = (provinceId: string) => { @@ -123,39 +166,119 @@ export const ProfileForm: React.FC = ({ setShowPasswordConf(!showPasswordConf); }; + // const onSubmit = async (data: RegistrationFormData) => { + // try { + // let instituteId = 1; + + // // Handle custom institute for journalists + // if (category === "6" && isCustomInstitute) { + // if (!customInstituteName.trim() || !instituteAddress.trim()) { + // onError?.("Please fill in all institute details"); + // return; + // } + + // const instituteData: InstituteData = { + // id: "0", + // name: customInstituteName, + // address: instituteAddress, + // }; + + // instituteId = await saveInstitute(instituteData); + // } else if (category === "6" && selectedInstitute) { + // instituteId = Number(selectedInstitute); + // } + + // const success = await submitRegistration( + // data, + // category, + // userData, + // instituteId + // ); + + // if (success) { + // onSuccess?.(data); + // } + // } catch (error: any) { + // onError?.(error.message || "Registration failed"); + // } + // }; + const onSubmit = async (data: RegistrationFormData) => { + // 🔹 TAMPILKAN LOADING + MySwal.fire({ + title: "Processing", + text: "Pleasewait", + allowOutsideClick: false, + allowEscapeKey: false, + didOpen: () => { + Swal.showLoading(); + }, + }); + try { - let instituteId = 1; + let instituteId = 1; // default (non-journalist / personel) - // Handle custom institute for journalists - if (category === "6" && isCustomInstitute) { - if (!customInstituteName.trim() || !instituteAddress.trim()) { - onError?.("Please fill in all institute details"); - return; + // khusus journalist + if (category === "6") { + if (isCustomInstitute) { + if (!customInstituteName.trim() || !instituteAddress.trim()) { + Swal.close(); + onError?.("Please fill in all institute details"); + return; + } + + const instituteData: InstituteData = { + id: "0", + name: customInstituteName, + address: instituteAddress, + }; + + instituteId = await saveInstitute(instituteData); + } else if (selectedInstitute) { + instituteId = Number(selectedInstitute); } - - const instituteData: InstituteData = { - id: "0", - name: customInstituteName, - address: instituteAddress, - }; - - instituteId = await saveInstitute(instituteData); - } else if (category === "6" && selectedInstitute) { - instituteId = Number(selectedInstitute); } + const payload = { + ...data, + email: otpIdentity.email, + nrp: otpIdentity.nrp, + personelName: otpIdentity.personelName, + }; + const success = await submitRegistration( - data, + payload, category, - userData, + otpIdentity.email, instituteId ); + // 🔹 TUTUP LOADING + Swal.close(); + if (success) { - onSuccess?.(data); + sessionStorage.removeItem("registration_identity"); + + await Swal.fire({ + icon: "success", + title:"Success", + text: "Register Success !!", + confirmButtonColor: "#dc3545", + }); + + onSuccess?.(payload); } } catch (error: any) { + // 🔹 TUTUP LOADING + Swal.close(); + + Swal.fire({ + icon: "error", + title: "Failed", + text: error?.message || "Registration failed", + confirmButtonColor: "#dc3545", + }); + onError?.(error.message || "Registration failed"); } }; @@ -240,7 +363,7 @@ export const ProfileForm: React.FC = ({ : "NRP"} * - = ({ "" } disabled + /> */} + )} {/* Personal Information */} -
+ {category === "7" && ( +
+ + { + const name = e.target.value; + + // set nama + setValue("firstName", name); + + // auto-generate username + const username = name + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); + + setValue("username", username); + }} + /> + {errors.firstName && ( +
+ {errors.firstName.message} +
+ )} +
+ )} + + {/*
)} -
+ */}
- = ({ placeholder={t("enterUsername", { defaultValue: "Enter username", })} + // onChange={(e) => { + // const value = e.target.value + // .replace(/[^\w.-]/g, "") + // .toLowerCase(); + // setValue("username", value); + // }} onChange={(e) => { const value = e.target.value - .replace(/[^\w.-]/g, "") - .toLowerCase(); + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); + + setValue("username", value); + }} + /> */} + { + setUsernameTouched(true); + const value = e.target.value + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); setValue("username", value); }} /> + {errors.username && (
{errors.username.message} @@ -309,7 +494,7 @@ export const ProfileForm: React.FC = ({ - = ({ placeholder="Enter your email" value={userData || ""} disabled - /> + /> */} + + {errors.email && (
{errors.email.message} diff --git a/types/registration.ts b/types/registration.ts index a70ca204..f37d26e7 100644 --- a/types/registration.ts +++ b/types/registration.ts @@ -7,7 +7,7 @@ export const registrationSchema = z.object({ .min(1, { message: "Full name is required" }) .min(2, { message: "Full name must be at least 2 characters" }) .max(100, { message: "Full name must be less than 100 characters" }) - .regex(/^[a-zA-Z\s]+$/, { message: "Full name can only contain letters and spaces" }), + .regex(/^[a-zA-Z\s.,]+$/, { message: "Full name can only contain letters and spaces" }), username: z .string() .min(1, { message: "Username is required" })