// ─── pages/users/EditUser.jsx ───────────────────────────────────────────────── import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useUsers } from "@/contexts/AdminUserContext"; import { toast } from "sonner"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ArrowLeft, Save, Plus, Trash2, UserCircle2 } from "lucide-react"; // ─── Schema ─────────────────────────────────────────────────────────────────── const addressSchema = z.object({ address_type: z.enum(["home", "work", "province", "other"]), street: z.string().min(1, "Street is required."), city: z.string().min(1, "City is required."), state: z.string().min(1, "State is required."), country: z.string().min(1, "Country is required."), zip: z.string().min(1, "ZIP is required."), }); const phoneSchema = z.object({ phone_type: z.enum(["mobile", "home", "work", "other"]), country_code: z.string().min(1, "Country code is required."), number: z.string().min(1, "Number is required."), }); const editUserSchema = z.object({ acc_type: z.enum(["user", "staff", "admin"]), is_active: z.boolean(), given_name: z.string().min(1, "First name is required."), middle_name: z.string().optional(), last_name: z.string().min(1, "Last name is required."), extension_name: z.string().optional(), date_of_birth: z.string().optional(), occupation: z.string().optional(), addresses: z.array(addressSchema).optional(), phone_number: z.array(phoneSchema).optional(), }); // ─── Defaults ───────────────────────────────────────────────────────────────── const DEFAULT_ADDRESS = { address_type: "home", street: "", city: "", state: "", country: "", zip: "", }; const DEFAULT_PHONE = { phone_type: "mobile", country_code: "63", number: "", }; export default function EditUser() { const { id } = useParams(); const navigate = useNavigate(); const { user, fetchUser, updateUser, loading } = useUsers(); const [avatarPreview, setAvatarPreview] = useState(null); const [avatarFile, setAvatarFile] = useState(null); const { register, handleSubmit, control, reset, formState: { errors, isDirty }, } = useForm({ resolver: zodResolver(editUserSchema), defaultValues: { acc_type: "user", is_active: true, given_name: "", middle_name: "", last_name: "", extension_name: "", date_of_birth: "", occupation: "", addresses: [], phone_number: [], }, }); // ─── Field arrays ────────────────────────────────────────────────────────── const { fields: addressFields, append: appendAddress, remove: removeAddress, } = useFieldArray({ control, name: "addresses" }); const { fields: phoneFields, append: appendPhone, remove: removePhone, } = useFieldArray({ control, name: "phone_number" }); // ─── Fetch user ──────────────────────────────────────────────────────────── useEffect(() => { fetchUser(id); }, [id]); // ─── Populate form ──────────────────────────────────────────────────────── useEffect(() => { if (!user) return; const info = user.personal_info ?? {}; const name = info.name ?? {}; setAvatarPreview(info.avatar?.url ?? null); reset({ acc_type: user.acc_type ?? "user", is_active: user.is_active ?? true, given_name: name.given_name ?? "", middle_name: name.middle_name ?? "", last_name: name.last_name ?? "", extension_name: name.extension_name ?? "", date_of_birth: info.date_of_birth ?? "", occupation: info.occupation ?? "", addresses: (info.addresses ?? []).map(({ full_address: _, ...rest }) => rest), phone_number: (info.phone_number ?? []).map(({ full_number: _, ...rest }) => rest), }); }, [user, reset]); // ─── Avatar preview ─────────────────────────────────────────────────────── const handleAvatarChange = (e) => { const file = e.target.files?.[0]; if (!file) return; setAvatarFile(file); setAvatarPreview(URL.createObjectURL(file)); }; // ─── Submit ─────────────────────────────────────────────────────────────── const onSubmit = async (values) => { const payload = { acc_type: values.acc_type, is_active: values.is_active, personal_info: { ...user.personal_info, date_of_birth: values.date_of_birth, occupation: values.occupation, name: { ...user.personal_info?.name, given_name: values.given_name, middle_name: values.middle_name, last_name: values.last_name, extension_name: values.extension_name, }, addresses: values.addresses, phone_number: values.phone_number, // ─── avatar handled separately via file upload API ──────────────── }, }; const res = await updateUser(id, payload); if (res) { toast("User updated successfully.", { action: { label: "Close", onClick: () => {} } }); navigate(`../view/${id}`); } else { toast("Failed to update user.", { action: { label: "Close", onClick: () => {} } }); } }; if (loading && !user) return (
); return (
{/* ─── Header ──────────────────────────────────────────────────────── */}

Edit User

{user?.email}

{/* ─── Avatar ──────────────────────────────────────────────────────── */}
{/* Preview */}
{avatarPreview ? Avatar : }
{/* Upload */}

JPG, PNG or WEBP. Avatar upload requires a separate file upload API.

{/* ─── Account Information ─────────────────────────────────────────── */} ( )} /> ( )} /> {/* ─── Personal Information ─────────────────────────────────────────── */} {/* ─── Addresses ───────────────────────────────────────────────────── */}

Addresses

{addressFields.length === 0 && (

No addresses added.

)} {addressFields.map((field, i) => (
{/* ─── Address Header ──────────────────────────────────────── */}
Address {i + 1}
( )} />
))}
{/* ─── Phone Numbers ───────────────────────────────────────────────── */}

Phone Numbers

{phoneFields.length === 0 && (

No phone numbers added.

)} {phoneFields.map((field, i) => (
Phone {i + 1}
( )} />
))}
); } // ─── Reusable EditSection ───────────────────────────────────────────────────── function EditSection({ title, children }) { return (

{title}

{children}
); } // ─── Reusable EditField ─────────────────────────────────────────────────────── function EditField({ label, error, required, children }) { return (
{children} {error &&

{error}

}
); }