TorbatYar/frontend/modules/beauty/features/owner/staff.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

201 lines
6.6 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, "سازمان الزامی است"),
branch_id: z.string().optional(),
code: z.string().min(1, "کد الزامی است"),
display_name: z.string().min(1, "نام الزامی است"),
kind: z.enum(["stylist", "therapist", "receptionist", "manager", "other"]).optional(),
mobile: z.string().optional(),
email: z.string().optional(),
});
const kindLabel: Record<string, string> = {
stylist: "آرایشگر",
therapist: "درمانگر",
receptionist: "پذیرش",
manager: "مدیر",
other: "سایر",
};
export default function StaffPage() {
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 branchesQ = useQuery({
queryKey: ["beauty", tenantId, "branches"],
queryFn: () => beautyBusinessApi.branches.list(tenantId!),
enabled: !!tenantId,
});
const listQ = useQuery({
queryKey: ["beauty", tenantId, "staff"],
queryFn: () => beautyBusinessApi.staff.list(tenantId!),
enabled: !!tenantId,
});
const createForm = useForm<z.infer<typeof createSchema>>({
resolver: zodResolver(createSchema),
defaultValues: {
organization_id: "",
branch_id: "",
code: "",
display_name: "",
kind: "stylist",
mobile: "",
email: "",
},
});
const selectedOrg = createForm.watch("organization_id");
const filteredBranches = (branchesQ.data ?? []).filter(
(b) => !selectedOrg || b.organization_id === selectedOrg
);
const invalidate = () => qc.invalidateQueries({ queryKey: ["beauty", tenantId, "staff"] });
const createM = useMutation({
mutationFn: (d: z.infer<typeof createSchema>) =>
beautyBusinessApi.staff.create(tenantId!, {
organization_id: d.organization_id,
branch_id: d.branch_id || undefined,
code: d.code,
display_name: d.display_name,
kind: d.kind,
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: "kind",
header: "نقش",
render: (r) => kindLabel[String(r.kind)] ?? String(r.kind),
},
{ key: "mobile", header: "موبایل" },
{
key: "status",
header: "وضعیت",
render: (r) => <Badge>{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="شعبه">
<Select {...createForm.register("branch_id")}>
<option value=""></option>
{filteredBranches.map((b) => (
<option key={b.id} value={b.id}>
{b.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="نقش">
<Select {...createForm.register("kind")}>
{Object.entries(kindLabel).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
</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>
);
}