fix: adjust register to polri member

This commit is contained in:
Sabda Yagra 2025-12-18 10:00:24 +07:00
parent 6a00c7b67d
commit ba03898e8b
3 changed files with 389 additions and 64 deletions

View File

@ -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<IdentityFormProps> = ({
category,
@ -28,11 +28,48 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
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<string>("");
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<IdentityFormProps> = ({
formState: { errors, isSubmitting },
setValue,
watch,
} = useForm<JournalistRegistrationData | PersonnelRegistrationData | GeneralRegistrationData>({
} = 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 = () => (
<>
<div className="flex flex-col px-0 lg:px-8 mb-4">
<Label htmlFor="association" className="mb-2">
{t("member", { defaultValue: "Member" })} <span className="text-red-500">*</span>
{t("member", { defaultValue: "Member" })}{" "}
<span className="text-red-500">*</span>
</Label>
<select
className={`py-2 px-3 rounded-md border text-sm border-slate-300 bg-white dark:bg-slate-600`}
@ -125,12 +237,17 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
<div className="px-0 lg:px-[34px] mb-4">
<Label htmlFor="journalistCertificate">
{t("journalistNumber", { defaultValue: "Journalist Certificate Number" })} <span className="text-red-500">*</span>
{t("journalistNumber", {
defaultValue: "Journalist Certificate Number",
})}{" "}
<span className="text-red-500">*</span>
</Label>
<Input
className={`mt-2 ${memberIdentityError ? "border-red-500" : ""}`}
autoComplete="off"
placeholder={t("inputJournalist", { defaultValue: "Enter journalist certificate number" })}
placeholder={t("inputJournalist", {
defaultValue: "Enter journalist certificate number",
})}
type="text"
value={memberIdentity}
onChange={(e) => handleMemberIdentityChange(e.target.value)}
@ -142,10 +259,13 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
</>
);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const renderPersonnelFields = () => (
<div className="px-0 lg:px-[34px] mb-4">
<Label htmlFor="policeNumber">
<b>{t("policeNumber", { defaultValue: "Police Number" })}</b> <span className="text-red-500">*</span>
<b>{t("policeNumber", { defaultValue: "Police Number" })}</b>{" "}
<span className="text-red-500">*</span>
</Label>
<Input
className={`mt-2 ${memberIdentityError ? "border-red-500" : ""}`}
@ -158,6 +278,17 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
{memberIdentityError && (
<p className="text-red-500 text-sm mt-1">{memberIdentityError}</p>
)}
{personelName && (
<div className="mt-3">
<Label>Nama Personel</Label>
<Input
value={personelName}
disabled
className="mt-2 bg-slate-100 dark:bg-slate-700"
/>
</div>
)}
</div>
);
@ -177,19 +308,26 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
<Input
className={`w-full ${errors.email ? "border-red-500" : ""}`}
autoComplete="off"
placeholder={t("inputEmail", { defaultValue: "Enter your email" })}
placeholder={t("inputEmail", {
defaultValue: "Enter your email",
})}
type="email"
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-red-500 mt-1">{errors.email.message}</p>
<p className="text-sm text-red-500 mt-1">
{errors.email.message}
</p>
)}
</div>
{/* Terms and conditions */}
<div className="text-center mb-2 px-[34px]">
<p className="text-sm lg:text-base">
{t("byRegis", { defaultValue: "By registering, you agree to our" })} <br />{" "}
{t("byRegis", {
defaultValue: "By registering, you agree to our",
})}{" "}
<br />{" "}
<a href="/privacy" target="_blank" className="text-red-500">
<b>{t("terms", { defaultValue: "Terms of Service" })}</b>
</a>{" "}
@ -221,4 +359,4 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
</form>
</div>
);
};
};

View File

@ -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<ProfileFormProps> = ({
userData,
category,
@ -60,6 +65,15 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
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<ProfileFormProps> = ({
} = useForm<RegistrationFormData>({
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<ProfileFormProps> = ({
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<ProfileFormProps> = ({
: "NRP"}
<span className="text-red-500">*</span>
</Label>
<Input
{/* <Input
type="text"
autoComplete="off"
className="mb-3"
@ -253,12 +376,53 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
""
}
disabled
/> */}
<Input
type="text"
autoComplete="off"
className="mb-3"
value={category === "7" ? otpIdentity.nrp || "" : ""}
disabled
/>
</div>
)}
{/* Personal Information */}
<div className="mb-4 px-0 lg:px-[34px]">
{category === "7" && (
<div className="px-0 lg:px-[34px] mb-4">
<Label className="mb-2">
Nama Personel <span className="text-red-500">*</span>
</Label>
<Input
type="text"
autoComplete="off"
className={errors.firstName ? "border-red-500" : ""}
{...register("firstName")}
onChange={(e) => {
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 && (
<div className="text-red-500 text-sm mt-1">
{errors.firstName.message}
</div>
)}
</div>
)}
{/* <div className="mb-4 px-0 lg:px-[34px]">
<Label className="mb-2">
{t("fullName", { defaultValue: "Full Name" })}{" "}
<span className="text-red-500">*</span>
@ -277,13 +441,13 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
{errors.firstName.message}
</div>
)}
</div>
</div> */}
<div className="mb-4 px-0 lg:px-[34px]">
<Label className="mb-2">
Username <span className="text-red-500">*</span>
</Label>
<Input
{/* <Input
type="text"
autoComplete="off"
className={errors.username ? "border-red-500" : ""}
@ -291,13 +455,34 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
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);
}}
/> */}
<Input
{...register("username")}
onChange={(e) => {
setUsernameTouched(true);
const value = e.target.value
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
setValue("username", value);
}}
/>
{errors.username && (
<div className="text-red-500 text-sm mt-1">
{errors.username.message}
@ -309,7 +494,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
<Label className="mb-2">
Email <span className="text-red-500">*</span>
</Label>
<Input
{/* <Input
type="email"
autoComplete="off"
className={errors.email ? "border-red-500" : ""}
@ -317,7 +502,9 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
placeholder="Enter your email"
value={userData || ""}
disabled
/>
/> */}
<Input {...register("email")} value={otpIdentity.email} disabled />
{errors.email && (
<div className="text-red-500 text-sm mt-1">
{errors.email.message}

View File

@ -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" })