|
|
|
@@ -0,0 +1,484 @@
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
|
import { useForm, useFieldArray, Controller } from "react-hook-form";
|
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
import { ChevronRight, ChevronLeft, Check, User, FileText } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
import { useUsers } from "@/contexts/AdminUserContext";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
|
|
|
|
|
// ─── Zod schema ───────────────────────────────────────────────────────────────
|
|
|
|
|
const phoneSchema = z.object({
|
|
|
|
|
phone_type: z.enum(["mobile", "home", "work"]),
|
|
|
|
|
country_code: z.string().min(1, "Required"),
|
|
|
|
|
number: z.string().min(1, "Number is required").regex(/^\d+$/, "Digits only"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const addressSchema = z.object({
|
|
|
|
|
address_type: z.enum(["home", "province", "work"]),
|
|
|
|
|
street: z.string().min(1, "Street is required"),
|
|
|
|
|
city: z.string().min(1, "City is required"),
|
|
|
|
|
state: z.string().min(1, "State is required"),
|
|
|
|
|
zip: z.string().min(1, "ZIP is required"),
|
|
|
|
|
country: z.string().min(1, "Country is required"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const staffUserSchema = z.object({
|
|
|
|
|
email: z.string().min(1, "Email is required").email("Invalid email format"),
|
|
|
|
|
last_name: z.string().min(1, "Last name is required"),
|
|
|
|
|
given_name: z.string().min(1, "Given name is required"),
|
|
|
|
|
middle_name: z.string().optional(),
|
|
|
|
|
extension_name: z.string().optional(),
|
|
|
|
|
date_of_birth: z.string().optional(),
|
|
|
|
|
occupation: z.string().optional(),
|
|
|
|
|
phones: z.array(phoneSchema).min(1, "At least one phone number is required"),
|
|
|
|
|
addresses: z.array(addressSchema).min(1, "At least one address is required"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ─── Steps ────────────────────────────────────────────────────────────────────
|
|
|
|
|
const STEPS = [
|
|
|
|
|
{ id: 0, label: "Personal Information", icon: User },
|
|
|
|
|
{ id: 1, label: "Summary", icon: FileText },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ─── Default values ───────────────────────────────────────────────────────────
|
|
|
|
|
const DEFAULT_VALUES = {
|
|
|
|
|
email: "",
|
|
|
|
|
last_name: "",
|
|
|
|
|
given_name: "",
|
|
|
|
|
middle_name: "",
|
|
|
|
|
extension_name: "",
|
|
|
|
|
date_of_birth: "",
|
|
|
|
|
occupation: "",
|
|
|
|
|
phones: [{ phone_type: "mobile", country_code: "63", number: "" }],
|
|
|
|
|
addresses: [{ address_type: "home", street: "", city: "", state: "", zip: "", country: "Philippines" }],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ─── Field wrapper ────────────────────────────────────────────────────────────
|
|
|
|
|
function Field({ label, required, error, children }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-sm font-medium">
|
|
|
|
|
{label}{required && <span className="text-destructive ml-0.5">*</span>}
|
|
|
|
|
</Label>
|
|
|
|
|
{children}
|
|
|
|
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Step 1 — Personal Information ───────────────────────────────────────────
|
|
|
|
|
function StepPersonal({ control, register, errors }) {
|
|
|
|
|
const {
|
|
|
|
|
fields: phoneFields,
|
|
|
|
|
append: appendPhone,
|
|
|
|
|
remove: removePhone,
|
|
|
|
|
} = useFieldArray({ control, name: "phones" });
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
fields: addressFields,
|
|
|
|
|
append: appendAddress,
|
|
|
|
|
remove: removeAddress,
|
|
|
|
|
} = useFieldArray({ control, name: "addresses" });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
|
|
|
|
{/* ── Name ── */}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Name</p>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<Field label="Last Name" required error={errors.last_name?.message}>
|
|
|
|
|
<Input {...register("last_name")} placeholder="Dela Cruz" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Given Name" required error={errors.given_name?.message}>
|
|
|
|
|
<Input {...register("given_name")} placeholder="Juan" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Middle Name" error={errors.middle_name?.message}>
|
|
|
|
|
<Input {...register("middle_name")} placeholder="Santos" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Extension Name" error={errors.extension_name?.message}>
|
|
|
|
|
<Input {...register("extension_name")} placeholder="Jr., Sr., III" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Date of Birth" error={errors.date_of_birth?.message}>
|
|
|
|
|
<Input type="date" {...register("date_of_birth")} />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Occupation" error={errors.occupation?.message}>
|
|
|
|
|
<Input {...register("occupation")} placeholder="Sales Agent" />
|
|
|
|
|
</Field>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* ── Credentials ── */}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Account Credentials</p>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
<Field label="Email" required error={errors.email?.message}>
|
|
|
|
|
<Input type="email" {...register("email")} placeholder="juan@example.com" />
|
|
|
|
|
</Field>
|
|
|
|
|
<div className="flex items-center gap-2 rounded-lg border border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground sm:col-span-2">
|
|
|
|
|
<span>🔐</span>
|
|
|
|
|
<span>A temporary password will be auto-generated and sent to the provided email address.</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* ── Phone Numbers ── */}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Phone Numbers</p>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{phoneFields.map((field, i) => (
|
|
|
|
|
<div key={field.id} className="border border-border rounded-lg p-4 space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Phone {i + 1}</span>
|
|
|
|
|
{phoneFields.length > 1 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-destructive h-7 px-2"
|
|
|
|
|
onClick={() => removePhone(i)}
|
|
|
|
|
>
|
|
|
|
|
Remove
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
|
|
|
<Field label="Type" error={errors.phones?.[i]?.phone_type?.message}>
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name={`phones.${i}.phone_type`}
|
|
|
|
|
render={({ field: f }) => (
|
|
|
|
|
<Select value={f.value} onValueChange={f.onChange}>
|
|
|
|
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="mobile">Mobile</SelectItem>
|
|
|
|
|
<SelectItem value="home">Home</SelectItem>
|
|
|
|
|
<SelectItem value="work">Work</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Country Code" error={errors.phones?.[i]?.country_code?.message}>
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name={`phones.${i}.country_code`}
|
|
|
|
|
render={({ field: f }) => (
|
|
|
|
|
<Select value={f.value} onValueChange={f.onChange}>
|
|
|
|
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="63">+63 (PH)</SelectItem>
|
|
|
|
|
<SelectItem value="1">+1 (US)</SelectItem>
|
|
|
|
|
<SelectItem value="44">+44 (UK)</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Number" required error={errors.phones?.[i]?.number?.message}>
|
|
|
|
|
<Input {...register(`phones.${i}.number`)} placeholder="9123456789" />
|
|
|
|
|
</Field>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="w-full"
|
|
|
|
|
onClick={() => appendPhone({ phone_type: "mobile", country_code: "63", number: "" })}
|
|
|
|
|
>
|
|
|
|
|
+ Add Phone Number
|
|
|
|
|
</Button>
|
|
|
|
|
{errors.phones?.root?.message && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.phones.root.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
{/* ── Addresses ── */}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-3">Addresses</p>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{addressFields.map((field, i) => (
|
|
|
|
|
<div key={field.id} className="border border-border rounded-lg p-4 space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Address {i + 1}</span>
|
|
|
|
|
{addressFields.length > 1 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-destructive h-7 px-2"
|
|
|
|
|
onClick={() => removeAddress(i)}
|
|
|
|
|
>
|
|
|
|
|
Remove
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
|
|
|
<Field label="Address Type" error={errors.addresses?.[i]?.address_type?.message}>
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name={`addresses.${i}.address_type`}
|
|
|
|
|
render={({ field: f }) => (
|
|
|
|
|
<Select value={f.value} onValueChange={f.onChange}>
|
|
|
|
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="home">Home</SelectItem>
|
|
|
|
|
<SelectItem value="province">Province</SelectItem>
|
|
|
|
|
<SelectItem value="work">Work</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Street" required error={errors.addresses?.[i]?.street?.message}>
|
|
|
|
|
<Input {...register(`addresses.${i}.street`)} placeholder="123 Mabini St" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="City" required error={errors.addresses?.[i]?.city?.message}>
|
|
|
|
|
<Input {...register(`addresses.${i}.city`)} placeholder="Manila" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="State / Region" required error={errors.addresses?.[i]?.state?.message}>
|
|
|
|
|
<Input {...register(`addresses.${i}.state`)} placeholder="NCR" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="ZIP Code" required error={errors.addresses?.[i]?.zip?.message}>
|
|
|
|
|
<Input {...register(`addresses.${i}.zip`)} placeholder="1000" />
|
|
|
|
|
</Field>
|
|
|
|
|
<Field label="Country" error={errors.addresses?.[i]?.country?.message}>
|
|
|
|
|
<Input {...register(`addresses.${i}.country`)} placeholder="Philippines" />
|
|
|
|
|
</Field>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="w-full"
|
|
|
|
|
onClick={() => appendAddress({ address_type: "province", street: "", city: "", state: "", zip: "", country: "Philippines" })}
|
|
|
|
|
>
|
|
|
|
|
+ Add Address
|
|
|
|
|
</Button>
|
|
|
|
|
{errors.addresses?.root?.message && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.addresses.root.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Step 2 — Summary ─────────────────────────────────────────────────────────
|
|
|
|
|
function SummaryRow({ label, value }) {
|
|
|
|
|
if (!value) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex justify-between py-1.5 text-sm">
|
|
|
|
|
<span className="text-muted-foreground min-w-[140px]">{label}</span>
|
|
|
|
|
<span className="text-foreground text-right">{value}</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function StepSummary({ data }) {
|
|
|
|
|
const fullName =
|
|
|
|
|
[data.last_name, [data.given_name, data.middle_name].filter(Boolean).join(" ")]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(", ") + (data.extension_name ? ` ${data.extension_name}` : "");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-1">
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<span className="text-sm font-medium">Personal Information</span>
|
|
|
|
|
<Badge variant="secondary" className="ml-auto text-xs">Staff</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<SummaryRow label="Full Name" value={fullName || "—"} />
|
|
|
|
|
<SummaryRow label="Email" value={data.email} />
|
|
|
|
|
<SummaryRow label="Date of Birth" value={data.date_of_birth} />
|
|
|
|
|
<SummaryRow label="Occupation" value={data.occupation} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{data.phones?.some((p) => p.number) && (
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-1">
|
|
|
|
|
<p className="text-sm font-medium mb-2">Phone Numbers</p>
|
|
|
|
|
{data.phones.filter((p) => p.number).map((p, i) => (
|
|
|
|
|
<SummaryRow
|
|
|
|
|
key={i}
|
|
|
|
|
label={p.phone_type.charAt(0).toUpperCase() + p.phone_type.slice(1)}
|
|
|
|
|
value={`+${p.country_code} ${p.number}`}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{data.addresses?.some((a) => a.street) && (
|
|
|
|
|
<div className="border border-border rounded-lg p-4 space-y-1">
|
|
|
|
|
<p className="text-sm font-medium mb-2">Addresses</p>
|
|
|
|
|
{data.addresses.filter((a) => a.street).map((a, i) => (
|
|
|
|
|
<SummaryRow
|
|
|
|
|
key={i}
|
|
|
|
|
label={a.address_type.charAt(0).toUpperCase() + a.address_type.slice(1)}
|
|
|
|
|
value={[a.street, a.city, a.state, a.country, a.zip].filter(Boolean).join(", ")}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Main Page ────────────────────────────────────────────────────────────────
|
|
|
|
|
export default function AddStaffUserPage() {
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { addStaffUser, loading } = useUsers();
|
|
|
|
|
const [step, setStep] = useState(0);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
register,
|
|
|
|
|
control,
|
|
|
|
|
trigger,
|
|
|
|
|
getValues,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
formState: { errors },
|
|
|
|
|
} = useForm({
|
|
|
|
|
resolver: zodResolver(staffUserSchema),
|
|
|
|
|
defaultValues: DEFAULT_VALUES,
|
|
|
|
|
mode: "onTouched",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Validate all fields then advance to summary
|
|
|
|
|
const handleNext = async () => {
|
|
|
|
|
const valid = await trigger();
|
|
|
|
|
if (valid) setStep(1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Called manually — no <form> tag so no accidental submit
|
|
|
|
|
const handleCreate = handleSubmit(async (data) => {
|
|
|
|
|
const payload = {
|
|
|
|
|
email: data.email,
|
|
|
|
|
personal_info: {
|
|
|
|
|
name: {
|
|
|
|
|
given_name: data.given_name,
|
|
|
|
|
last_name: data.last_name,
|
|
|
|
|
middle_name: data.middle_name ?? "",
|
|
|
|
|
extension_name: data.extension_name ?? "",
|
|
|
|
|
},
|
|
|
|
|
date_of_birth: data.date_of_birth || null,
|
|
|
|
|
occupation: data.occupation ?? "",
|
|
|
|
|
phone_number: data.phones,
|
|
|
|
|
addresses: data.addresses,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const res = await addStaffUser(payload);
|
|
|
|
|
if (res) navigate("/admin/users/all");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
// ← plain div, no <form> — prevents any accidental submit on button clicks
|
|
|
|
|
<div className="max-w-4xl mx-auto px-4 py-8 space-y-6">
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-xl font-semibold tracking-tight">Add Staff User</h1>
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
|
|
|
Creates a new account with staff access level.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Stepper */}
|
|
|
|
|
<div className="flex items-center gap-0">
|
|
|
|
|
{STEPS.map((s, i) => {
|
|
|
|
|
const Icon = s.icon;
|
|
|
|
|
const isActive = step === i;
|
|
|
|
|
const isDone = step > i;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={s.id} className="flex items-center flex-1 last:flex-none">
|
|
|
|
|
<div className="flex flex-col items-center gap-1">
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"h-8 w-8 rounded-full flex items-center justify-center border transition-colors",
|
|
|
|
|
isDone && "bg-emerald-600 border-emerald-600 text-white",
|
|
|
|
|
isActive && "border-primary bg-primary text-primary-foreground",
|
|
|
|
|
!isActive && !isDone && "border-border bg-background text-muted-foreground"
|
|
|
|
|
)}>
|
|
|
|
|
{isDone ? <Check className="h-4 w-4" /> : <Icon className="h-4 w-4" />}
|
|
|
|
|
</div>
|
|
|
|
|
<span className={cn(
|
|
|
|
|
"text-[11px] font-medium whitespace-nowrap hidden sm:block",
|
|
|
|
|
isActive ? "text-foreground" : "text-muted-foreground",
|
|
|
|
|
isDone ? "text-emerald-600" : ""
|
|
|
|
|
)}>
|
|
|
|
|
{s.label}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{i < STEPS.length - 1 && (
|
|
|
|
|
<div className={cn(
|
|
|
|
|
"flex-1 h-px mx-2 mb-4 transition-colors",
|
|
|
|
|
step > i ? "bg-emerald-600" : "bg-border"
|
|
|
|
|
)} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Step content */}
|
|
|
|
|
<div className="border border-border rounded-xl p-5 bg-card min-h-[320px]">
|
|
|
|
|
<h2 className="text-base font-medium mb-4">{STEPS[step].label}</h2>
|
|
|
|
|
{step === 0 && (
|
|
|
|
|
<StepPersonal control={control} register={register} errors={errors} />
|
|
|
|
|
)}
|
|
|
|
|
{step === 1 && (
|
|
|
|
|
<StepSummary data={getValues()} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Navigation */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={step === 0 ? () => navigate(-1) : () => setStep(0)}
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeft className="h-4 w-4 mr-1" />
|
|
|
|
|
{step === 0 ? "Cancel" : "Back"}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{step === 0 ? (
|
|
|
|
|
<Button type="button" onClick={handleNext}>
|
|
|
|
|
Next
|
|
|
|
|
<ChevronRight className="h-4 w-4 ml-1" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
type="button" // ← type="button", not "submit"
|
|
|
|
|
disabled={loading}
|
|
|
|
|
onClick={handleCreate} // ← called manually
|
|
|
|
|
className="bg-emerald-600 hover:bg-emerald-700 text-white"
|
|
|
|
|
>
|
|
|
|
|
{loading ? "Creating..." : "Create Staff User"}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|