diff --git a/src/app/(staff)/staff/clients/add/page.tsx b/src/app/(staff)/staff/clients/add/page.tsx
new file mode 100644
index 0000000..4e6e59d
--- /dev/null
+++ b/src/app/(staff)/staff/clients/add/page.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from "next";
+import { redirect } from "next/navigation";
+import { TopNav } from "@/components/navigation/top-nav";
+import { getActingBusinessContext } from "@/lib/business/get-acting-business-context";
+import { AddClientForm } from "@/features/staff/components/add-client-form";
+
+export const metadata: Metadata = { title: "Add Client" };
+export const dynamic = "force-dynamic";
+
+export default async function AddClientPage() {
+ const context = await getActingBusinessContext();
+ if (!context) redirect("/home");
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(staff)/staff/clients/page.tsx b/src/app/(staff)/staff/clients/page.tsx
index 8a2005d..11a214c 100644
--- a/src/app/(staff)/staff/clients/page.tsx
+++ b/src/app/(staff)/staff/clients/page.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { Search, Users } from "lucide-react";
+import { Plus, Search, Users } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { EmptyState } from "@/components/feedback/empty-state";
@@ -78,6 +78,15 @@ export default async function StaffClientsPage({
sticky
className="bg-stone"
leftAction={}
+ action={
+
+
+
+ }
/>
diff --git a/src/features/staff/actions/clients.ts b/src/features/staff/actions/clients.ts
new file mode 100644
index 0000000..cb7eebd
--- /dev/null
+++ b/src/features/staff/actions/clients.ts
@@ -0,0 +1,56 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { registerRequestSchema } from "@/features/auth/schemas";
+import { getActingBusinessContext } from "@/lib/business/get-acting-business-context";
+import { writeAuditLog } from "@/lib/audit/write-audit-log";
+import { prisma } from "@/lib/db/prisma";
+import { createCustomerByStaff } from "@/services/customer/customer.service";
+
+export interface AddClientInput {
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone?: string;
+}
+
+export interface AddClientResult {
+ success: boolean;
+ error?: string;
+ customerId?: string;
+}
+
+/**
+ * Open to any signed-in staff/owner/super-admin — the same gate the
+ * Clients page itself uses (see (staff)/staff/clients/page.tsx), not
+ * owner-only: a walk-in's details are just as often taken by whoever's
+ * on the floor as by the owner.
+ */
+export async function addClientAction(input: AddClientInput): Promise {
+ const context = await getActingBusinessContext();
+ if (!context) return { success: false, error: "You must be signed in as staff." };
+
+ const parsed = registerRequestSchema.safeParse(input);
+ if (!parsed.success) {
+ return { success: false, error: parsed.error.issues[0]?.message ?? "Please check the form." };
+ }
+
+ const result = await createCustomerByStaff(prisma, parsed.data);
+ if (!result.success) return result;
+
+ await writeAuditLog(prisma, {
+ actorUserId: context.userId,
+ action: "CUSTOMER_ADDED_BY_STAFF",
+ entityType: "CustomerProfile",
+ entityId: result.customerId!,
+ metadata: { email: parsed.data.email },
+ });
+
+ // The new client won't actually show up in the Clients list until they
+ // have an appointment (see that page's own "belongs to this business is
+ // via the appointment relation" note) — revalidated anyway so the list
+ // is fresh the moment that happens, without a stale cache lingering.
+ revalidatePath("/staff/clients");
+
+ return result;
+}
diff --git a/src/features/staff/components/add-client-form.tsx b/src/features/staff/components/add-client-form.tsx
new file mode 100644
index 0000000..28ab352
--- /dev/null
+++ b/src/features/staff/components/add-client-form.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState, useTransition } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { addClientAction } from "../actions/clients";
+
+/**
+ * A walk-in's details taken over the counter — creates a real account
+ * (see createCustomerByStaff) rather than a guest profile, so the
+ * customer can sign in later with just their email, no password. They're
+ * never emailed anything about this; the app only reaches out once they
+ * ask to sign in themselves.
+ */
+export function AddClientForm() {
+ const router = useRouter();
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [email, setEmail] = useState("");
+ const [phone, setPhone] = useState("");
+ const [error, setError] = useState(null);
+ const [added, setAdded] = useState<{ customerId: string; name: string } | null>(null);
+ const [isPending, startTransition] = useTransition();
+
+ const submit = () => {
+ setError(null);
+ startTransition(async () => {
+ const result = await addClientAction({ firstName, lastName, email, phone: phone || undefined });
+ if (!result.success || !result.customerId) {
+ setError(result.error ?? "Couldn't add this client.");
+ return;
+ }
+ setAdded({ customerId: result.customerId, name: `${firstName} ${lastName}`.trim() });
+ });
+ };
+
+ if (added) {
+ return (
+
+
{added.name} has been added.
+
+ They can sign in anytime with just their email — no password, no invite email sent. They'll show up
+ in Clients once they have an appointment.
+