import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react"; import { useState, useRef } from "react"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Shield, ArrowLeft, ArrowRight, AlertCircle, Loader2, CheckCircle, Eye, EyeOff, Lock, Star } from "lucide-react"; import { validatePin } from "~/utils/auth-utils"; import pengelolaAuthService from "~/services/auth/pengelola.service"; import { getUserSession, createUserSession } from "~/sessions.server"; // Progress Indicator Component const ProgressIndicator = ({ currentStep = 5, totalSteps = 5 }) => { return (
{Array.from({ length: totalSteps }, (_, index) => { const stepNumber = index + 1; const isActive = stepNumber === currentStep; const isCompleted = stepNumber < currentStep; return (
{isCompleted || isActive ? ( ) : ( stepNumber )}
{stepNumber < totalSteps && (
)}
); })}
); }; // Interfaces interface LoaderData { userSession: any; } interface CreatePinActionData { errors?: { userpin?: string; confirmPin?: string; general?: string; }; success?: boolean; } export const loader = async ({ request }: LoaderFunctionArgs): Promise => { const userSession = await getUserSession(request); // Check if user is authenticated and has pengelola role if (!userSession || userSession.role !== "pengelola") { return redirect("/authpengelola"); } // Check if user should be on this step if (userSession.registrationStatus !== "approved") { // Redirect based on current status switch (userSession.registrationStatus) { case "uncomplete": return redirect("/authpengelola/completingcompanyprofile"); case "awaiting_approval": return redirect("/authpengelola/waitingapprovalfromadministrator"); case "complete": return redirect("/pengelola/dashboard"); default: break; } } return json({ userSession }); }; export const action = async ({ request }: ActionFunctionArgs): Promise => { const userSession = await getUserSession(request); if (!userSession || userSession.role !== "pengelola") { return redirect("/authpengelola"); } const formData = await request.formData(); const userpin = formData.get("userpin") as string; const confirmPin = formData.get("confirmPin") as string; // Validation const errors: { [key: string]: string } = {}; if (!userpin) { errors.userpin = "PIN wajib diisi"; } else if (!validatePin(userpin)) { errors.userpin = "PIN harus 6 digit angka"; } if (!confirmPin) { errors.confirmPin = "Konfirmasi PIN wajib diisi"; } else if (userpin !== confirmPin) { errors.confirmPin = "PIN dan konfirmasi PIN tidak sama"; } if (Object.keys(errors).length > 0) { return json({ errors }, { status: 400 }); } try { // Call API untuk create PIN const response = await pengelolaAuthService.createPin({ userpin }); if (response.meta.status === 200 && response.data) { // PIN berhasil dibuat, update session dan redirect ke dashboard return createUserSession({ request, sessionData: { ...userSession, accessToken: response.data.access_token, refreshToken: response.data.refresh_token, sessionId: response.data.session_id, tokenType: response.data.token_type, registrationStatus: response.data.registration_status, nextStep: response.data.next_step }, redirectTo: "/pengelola/dashboard" }); } else { return json( { errors: { general: response.meta.message || "Gagal membuat PIN" } }, { status: 400 } ); } } catch (error: any) { console.error("Create PIN error:", error); // Handle specific API errors if (error.response?.data?.meta?.message) { return json( { errors: { general: error.response.data.meta.message } }, { status: error.response.status || 500 } ); } return json( { errors: { general: "Gagal membuat PIN. Silakan coba lagi." } }, { status: 500 } ); } }; export default function CreateANewPin() { const { userSession } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [pin, setPin] = useState(["", "", "", "", "", ""]); const [confirmPin, setConfirmPin] = useState(["", "", "", "", "", ""]); const [showPin, setShowPin] = useState(false); const [showConfirmPin, setShowConfirmPin] = useState(false); const pinRefs = useRef<(HTMLInputElement | null)[]>([]); const confirmPinRefs = useRef<(HTMLInputElement | null)[]>([]); const isSubmitting = navigation.state === "submitting"; // Handle PIN input change const handlePinChange = ( index: number, value: string, isPinField: boolean = true ) => { if (!/^\d*$/.test(value)) return; // Only allow digits const currentPin = isPinField ? pin : confirmPin; const setCurrentPin = isPinField ? setPin : setConfirmPin; const refs = isPinField ? pinRefs : confirmPinRefs; const newPin = [...currentPin]; newPin[index] = value; setCurrentPin(newPin); // Auto-focus next input if (value && index < 5) { refs.current[index + 1]?.focus(); } }; // Handle key down (backspace) const handleKeyDown = ( index: number, e: React.KeyboardEvent, isPinField: boolean = true ) => { const currentPin = isPinField ? pin : confirmPin; const refs = isPinField ? pinRefs : confirmPinRefs; if (e.key === "Backspace" && !currentPin[index] && index > 0) { refs.current[index - 1]?.focus(); } }; // Handle paste const handlePaste = (e: React.ClipboardEvent, isPinField: boolean = true) => { e.preventDefault(); const pastedText = e.clipboardData.getData("text"); const digits = pastedText.replace(/\D/g, "").slice(0, 6); if (digits.length === 6) { const newPin = digits.split(""); if (isPinField) { setPin(newPin); pinRefs.current[5]?.focus(); } else { setConfirmPin(newPin); confirmPinRefs.current[5]?.focus(); } } }; const pinValue = pin.join(""); const confirmPinValue = confirmPin.join(""); const isPinComplete = pinValue.length === 6 && confirmPinValue.length === 6; const isPinMatching = pinValue === confirmPinValue && pinValue.length === 6; return (
{/* Progress Indicator */} {/* Main Card */}

Buat PIN Keamanan

Langkah terakhir! Buat PIN 6 digit untuk mengamankan akun Anda

{/* Error Alert */} {actionData?.errors?.general && ( {actionData.errors.general} )} {/* Form */}
{/* PIN Input */}
{pin.map((digit, index) => ( (pinRefs.current[index] = el)} type={showPin ? "text" : "password"} maxLength={1} value={digit} onChange={(e) => handlePinChange(index, e.target.value, true) } onKeyDown={(e) => handleKeyDown(index, e, true)} onPaste={(e) => handlePaste(e, true)} className={`w-14 h-14 text-center text-xl font-bold ${ actionData?.errors?.userpin ? "border-red-500" : "" }`} autoFocus={index === 0} /> ))}
{actionData?.errors?.userpin && (

{actionData.errors.userpin}

)}
{/* Confirm PIN Input */}
{confirmPin.map((digit, index) => ( (confirmPinRefs.current[index] = el)} type={showConfirmPin ? "text" : "password"} maxLength={1} value={digit} onChange={(e) => handlePinChange(index, e.target.value, false) } onKeyDown={(e) => handleKeyDown(index, e, false)} onPaste={(e) => handlePaste(e, false)} className={`w-14 h-14 text-center text-xl font-bold ${ actionData?.errors?.confirmPin ? "border-red-500" : "" } ${ isPinComplete && !isPinMatching ? "border-red-500" : "" } ${isPinMatching ? "border-green-500" : ""}`} /> ))}
{actionData?.errors?.confirmPin && (

{actionData.errors.confirmPin}

)} {isPinComplete && !isPinMatching && !actionData?.errors?.confirmPin && (

PIN tidak sama, silakan periksa kembali

)} {isPinMatching && (
PIN cocok!
)}
{/* PIN Requirements */}

Syarat PIN Keamanan

• Harus terdiri dari 6 digit angka

• Hindari urutan angka (123456) atau angka sama (111111)

• Jangan gunakan tanggal lahir atau nomor telepon

• PIN akan digunakan untuk verifikasi transaksi penting

{/* Submit Button */}
{/* Back Link */}
Kembali ke status persetujuan
{/* Security Tips */}

🔒 Tips Keamanan PIN

• Jangan berikan PIN kepada siapa pun

• Ganti PIN secara berkala untuk keamanan optimal

• PIN dapat diubah melalui menu pengaturan setelah login

); }