TorbatYar/frontend/modules/beauty/features/owner/branches.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, type Branch } 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, "کد الزامی است"),
name: z.string().min(1, "نام الزامی است"),
description: z.string().optional(),
});
const statusLabel: Record<string, string> = {
draft: "پیش‌نویس",
active: "فعال",
inactive: "غیرفعال",
suspended: "معلق",
archived: "آرشیو",
};
export default function BranchesPage() {
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, "branches"],
queryFn: () => beautyBusinessApi.branches.list(tenantId!),
enabled: !!tenantId,
});
const createForm = useForm<z.infer<typeof createSchema>>({
resolver: zodResolver(createSchema),
defaultValues: { organization_id: "", code: "", name: "", description: "" },
});
const invalidate = () => qc.invalidateQueries({ queryKey: ["beauty", tenantId, "branches"] });
const createM = useMutation({
mutationFn: (d: z.infer<typeof createSchema>) => beautyBusinessApi.branches.create(tenantId!, d),
onSuccess: async () => {
toast.success("شعبه ایجاد شد");
setCreateOpen(false);
createForm.reset();
await invalidate();
},
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || listQ.isLoading || orgsQ.isLoading) return <LoadingState />;
if (listQ.error) {
return <ErrorState message={listQ.error.message} onRetry={() => listQ.refetch()} />;
}
const orgMap = Object.fromEntries((orgsQ.data ?? []).map((o) => [o.id, o.name]));
return (
<div>
<PageHeader
title="شعب"
description="مدیریت شعب سالن زیبایی."
actions={
<Button onClick={() => setCreateOpen(true)} disabled={(orgsQ.data?.length ?? 0) === 0}>
<Plus className="h-4 w-4" />
شعبه جدید
</Button>
}
/>
{(orgsQ.data?.length ?? 0) === 0 ? (
<EmptyState title="ابتدا یک سازمان ایجاد کنید" />
) : (
<DataTable
columns={[
{ key: "code", header: "کد" },
{ key: "name", header: "نام" },
{
key: "organization_id",
header: "سازمان",
render: (r) => orgMap[String(r.organization_id)] ?? "—",
},
{
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.name?.message}>
<Input {...createForm.register("name")} />
</FormField>
<FormField label="توضیحات">
<Input {...createForm.register("description")} />
</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>
);
}