TorbatYar/frontend/modules/loyalty/features/referrals/referrals.tsx
Mortezakoohjani d579d0b142 feat(loyalty): add Loyalty Platform Frontend module
Add frontend/modules/loyalty with types, API client, design system, feature pages and thin App Router routes under app/loyalty/. BFF proxy at app/api/loyalty/. Include loyalty frontend docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:50:55 +03:30

118 lines
4.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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button, Dialog, FormField, Input, PageHeader, EmptyState } from "@/components/ds";
import { loyaltyApi } from "@/modules/loyalty/services/loyalty-api";
import { useTenantId } from "@/hooks/useTenantId";
import { LoyaltyBreadcrumbs } from "@/modules/loyalty/components/LoyaltyBreadcrumbs";
import { LoyaltyPageLoader, LoyaltyPageError } from "@/modules/loyalty/pages/shared";
import { LoyaltyTablePage, LoyaltyStatusChip } from "@/modules/loyalty/design-system";
export default function ReferralsPage() {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [open, setOpen] = useState(false);
const [code, setCode] = useState("");
const [refereeId, setRefereeId] = useState("");
const q = useQuery({
queryKey: ["loyalty", tenantId, "referrals"],
queryFn: async () => {
const page = await loyaltyApi.referrals.list(tenantId!, { page: 1, page_size: 200 });
return "items" in page ? page.items : (page as never);
},
enabled: !!tenantId,
});
const attrM = useMutation({
mutationFn: () =>
loyaltyApi.referrals.attribute(tenantId!, {
code,
referee_member_id: refereeId,
}),
onSuccess: () => {
toast.success("ارجاع ثبت شد");
setOpen(false);
qc.invalidateQueries({ queryKey: ["loyalty", tenantId, "referrals"] });
},
onError: (e: Error) => toast.error(e.message),
});
const act = useMutation({
mutationFn: ({ id, action }: { id: string; action: "convert" | "reward" | "cancel" }) => {
if (action === "convert") return loyaltyApi.referrals.convert(tenantId!, id);
if (action === "reward") return loyaltyApi.referrals.reward(tenantId!, id);
return loyaltyApi.referrals.cancel(tenantId!, id, { reason: "cancelled from UI" });
},
onSuccess: () => {
toast.success("به‌روز شد");
qc.invalidateQueries({ queryKey: ["loyalty", tenantId, "referrals"] });
},
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || q.isLoading) return <LoyaltyPageLoader />;
if (q.error) return <LoyaltyPageError error={q.error} onRetry={() => q.refetch()} />;
return (
<div>
<LoyaltyBreadcrumbs items={[{ label: "ارجاعات" }]} />
<PageHeader
title="ارجاعات"
description="ثبت attribution و تبدیل/پاداش/لغو."
actions={<Button onClick={() => setOpen(true)}>ثبت ارجاع</Button>}
/>
<LoyaltyTablePage
title="Attributionها"
columns={[
{ key: "id", header: "شناسه" },
{
key: "status",
header: "وضعیت",
render: (r) => (
<LoyaltyStatusChip status={String(r.status)} domain="referral_attribution" />
),
},
{ key: "referrer_member_id", header: "معرف" },
{ key: "referee_member_id", header: "معرفی‌شونده" },
{
key: "actions",
header: "عملیات",
searchable: false,
render: (r) => (
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={() => act.mutate({ id: String(r.id), action: "convert" })}>
تبدیل
</Button>
<Button size="sm" variant="ghost" onClick={() => act.mutate({ id: String(r.id), action: "reward" })}>
پاداش
</Button>
<Button size="sm" variant="ghost" onClick={() => act.mutate({ id: String(r.id), action: "cancel" })}>
لغو
</Button>
</div>
),
},
]}
rows={(q.data ?? []) as unknown as Record<string, unknown>[]}
empty={<EmptyState title="ارجاعی نیست" />}
/>
<Dialog open={open} onClose={() => setOpen(false)} title="ثبت ارجاع">
<div className="space-y-3">
<FormField label="کد معرفی">
<Input value={code} onChange={(e) => setCode(e.target.value)} />
</FormField>
<FormField label="شناسه عضو معرفی‌شونده">
<Input value={refereeId} onChange={(e) => setRefereeId(e.target.value)} />
</FormField>
<Button loading={attrM.isPending} onClick={() => attrM.mutate()}>
ثبت
</Button>
</div>
</Dialog>
</div>
);
}