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

169 lines
5.5 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,
FormField,
Select,
Card,
CardContent,
Badge,
} from "@/components/ds";
const upsertSchema = z.object({
organization_id: z.string().optional(),
key: z.string().min(1, "کلید الزامی است"),
valueJson: z.string().optional(),
description: z.string().optional(),
});
export default function BeautySettingsPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const orgsQ = useQuery({
queryKey: ["beauty", tenantId, "organizations"],
queryFn: () => beautyBusinessApi.organizations.list(tenantId!),
enabled: !!tenantId,
});
const settingsQ = useQuery({
queryKey: ["beauty", tenantId, "settings"],
queryFn: () => beautyBusinessApi.settings.list(tenantId!),
enabled: !!tenantId,
});
const permissionsQ = useQuery({
queryKey: ["beauty", tenantId, "permissions-catalog"],
queryFn: () => beautyBusinessApi.permissions.catalog(tenantId!),
enabled: !!tenantId,
});
const form = useForm<z.infer<typeof upsertSchema>>({
resolver: zodResolver(upsertSchema),
defaultValues: { organization_id: "", key: "", valueJson: "{}", description: "" },
});
const invalidate = () =>
qc.invalidateQueries({ queryKey: ["beauty", tenantId, "settings"] });
const upsertM = useMutation({
mutationFn: (d: z.infer<typeof upsertSchema>) => {
let value: Record<string, unknown> | undefined;
try {
value = d.valueJson ? (JSON.parse(d.valueJson) as Record<string, unknown>) : undefined;
} catch {
throw new Error("JSON مقدار نامعتبر است");
}
return beautyBusinessApi.settings.upsert(tenantId!, {
organization_id: d.organization_id || undefined,
key: d.key,
value,
description: d.description || undefined,
});
},
onSuccess: async () => {
toast.success("تنظیم ذخیره شد");
setOpen(false);
form.reset();
await invalidate();
},
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || settingsQ.isLoading) return <LoadingState />;
if (settingsQ.error) {
return <ErrorState message={settingsQ.error.message} onRetry={() => settingsQ.refetch()} />;
}
return (
<div>
<PageHeader
title="تنظیمات"
description="کلید-مقدار تنظیمات سازمان و شعبه."
actions={
<Button onClick={() => setOpen(true)}>
<Plus className="h-4 w-4" />
تنظیم جدید
</Button>
}
/>
<DataTable
columns={[
{ key: "key", header: "کلید" },
{ key: "description", header: "توضیح" },
{
key: "value",
header: "مقدار",
render: (r) => (
<span className="text-xs text-[var(--muted)]">
{r.value ? JSON.stringify(r.value).slice(0, 60) : "—"}
</span>
),
},
]}
rows={(settingsQ.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState action={<Button onClick={() => setOpen(true)}>افزودن تنظیم</Button>} />}
/>
{permissionsQ.data ? (
<Card className="mt-8">
<CardContent className="pt-5">
<h2 className="mb-2 font-semibold text-secondary">کاتالوگ مجوزها</h2>
<Badge tone="default">{permissionsQ.data.count} مجوز</Badge>
</CardContent>
</Card>
) : null}
<Dialog open={open} onClose={() => setOpen(false)} title="تنظیم جدید">
<form className="space-y-3" onSubmit={form.handleSubmit((d) => upsertM.mutate(d))}>
<FormField label="سازمان (اختیاری)">
<Select {...form.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={form.formState.errors.key?.message}>
<Input {...form.register("key")} />
</FormField>
<FormField label="مقدار (JSON)">
<Input {...form.register("valueJson")} />
</FormField>
<FormField label="توضیح">
<Input {...form.register("description")} />
</FormField>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
انصراف
</Button>
<Button type="submit" disabled={upsertM.isPending}>
ذخیره
</Button>
</div>
</form>
</Dialog>
</div>
);
}