TorbatYar/frontend/modules/beauty/features/owner/customers.tsx
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:28:27 +03:30

157 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Plus } from "lucide-react";
import { beautyBusinessApi } from "@/modules/beauty/services/beauty-business-api";
import { useTenantId } from "@/hooks/useTenantId";
import {
PageHeader,
Button,
Dialog,
Input,
DataTable,
EmptyState,
LoadingState,
ErrorState,
Badge,
FormField,
Select,
} from "@/components/ds";
const createSchema = z.object({
organization_id: z.string().min(1, "سازمان الزامی است"),
code: z.string().min(1, "کد الزامی است"),
display_name: z.string().min(1, "نام الزامی است"),
mobile: z.string().optional(),
email: z.string().optional(),
});
const statusLabel: Record<string, string> = {
draft: "پیش‌نویس",
active: "فعال",
inactive: "غیرفعال",
suspended: "معلق",
archived: "آرشیو",
};
export default function CustomersPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const orgsQ = useQuery({
queryKey: ["beauty", tenantId, "organizations"],
queryFn: () => beautyBusinessApi.organizations.list(tenantId!),
enabled: !!tenantId,
});
const listQ = useQuery({
queryKey: ["beauty", tenantId, "customers"],
queryFn: () => beautyBusinessApi.customers.list(tenantId!),
enabled: !!tenantId,
});
const createForm = useForm<z.infer<typeof createSchema>>({
resolver: zodResolver(createSchema),
defaultValues: { organization_id: "", code: "", display_name: "", mobile: "", email: "" },
});
const invalidate = () => qc.invalidateQueries({ queryKey: ["beauty", tenantId, "customers"] });
const createM = useMutation({
mutationFn: (d: z.infer<typeof createSchema>) =>
beautyBusinessApi.customers.create(tenantId!, {
...d,
mobile: d.mobile || undefined,
email: d.email || undefined,
}),
onSuccess: async () => {
toast.success("مشتری ایجاد شد");
setCreateOpen(false);
createForm.reset();
await invalidate();
},
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || listQ.isLoading) return <LoadingState />;
if (listQ.error) {
return <ErrorState message={listQ.error.message} onRetry={() => listQ.refetch()} />;
}
return (
<div>
<PageHeader
title="مشتریان"
description="پروفایل مشتریان سالن زیبایی."
actions={
<Button onClick={() => setCreateOpen(true)}>
<Plus className="h-4 w-4" />
مشتری جدید
</Button>
}
/>
<DataTable
columns={[
{ key: "code", header: "کد" },
{ key: "display_name", header: "نام" },
{ key: "mobile", header: "موبایل" },
{ key: "email", header: "ایمیل" },
{
key: "status",
header: "وضعیت",
render: (r) => (
<Badge tone={r.status === "active" ? "success" : "default"}>
{statusLabel[String(r.status)] ?? String(r.status)}
</Badge>
),
},
]}
rows={(listQ.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState action={<Button onClick={() => setCreateOpen(true)}>ایجاد مشتری</Button>} />}
/>
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} title="مشتری جدید">
<form className="space-y-3" onSubmit={createForm.handleSubmit((d) => createM.mutate(d))}>
<FormField label="سازمان" error={createForm.formState.errors.organization_id?.message}>
<Select {...createForm.register("organization_id")}>
<option value="">انتخاب کنید</option>
{(orgsQ.data ?? []).map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</Select>
</FormField>
<FormField label="کد" error={createForm.formState.errors.code?.message}>
<Input {...createForm.register("code")} />
</FormField>
<FormField label="نام" error={createForm.formState.errors.display_name?.message}>
<Input {...createForm.register("display_name")} />
</FormField>
<FormField label="موبایل">
<Input {...createForm.register("mobile")} />
</FormField>
<FormField label="ایمیل">
<Input {...createForm.register("email")} />
</FormField>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
انصراف
</Button>
<Button type="submit" disabled={createM.isPending}>
ذخیره
</Button>
</div>
</form>
</Dialog>
</div>
);
}