TorbatYar/frontend/modules/loyalty/features/points/transactions.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

147 lines
5.7 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,
Select,
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 PointTransactionsPage() {
const { tenantId } = useTenantId();
const [accountId, setAccountId] = useState("");
const [active, setActive] = useState("");
const [op, setOp] = useState<"earn" | "redeem" | "adjust" | "expire" | null>(null);
const [amount, setAmount] = useState("10");
const [reason, setReason] = useState("");
const qc = useQueryClient();
const accountsQ = useQuery({
queryKey: ["loyalty", tenantId, "point-accounts"],
queryFn: () => loyaltyApi.pointAccounts.list(tenantId!, { page: 1, page_size: 500 }),
enabled: !!tenantId,
});
const balanceQ = useQuery({
queryKey: ["loyalty", tenantId, "point-balance", active],
queryFn: () => loyaltyApi.pointAccounts.balance(tenantId!, active),
enabled: !!tenantId && !!active,
});
const ledgerQ = useQuery({
queryKey: ["loyalty", tenantId, "point-ledger", active],
queryFn: () => loyaltyApi.pointAccounts.ledger(tenantId!, active, { offset: 0, limit: 100 }),
enabled: !!tenantId && !!active,
});
const mut = useMutation({
mutationFn: async () => {
const body = { amount: Number(amount), reason: reason || undefined };
if (op === "earn") return loyaltyApi.pointAccounts.earn(tenantId!, active, body);
if (op === "redeem") return loyaltyApi.pointAccounts.redeem(tenantId!, active, body);
if (op === "adjust")
return loyaltyApi.pointAccounts.adjust(tenantId!, active, {
amount: Number(amount),
reason: reason || "adjust",
});
return loyaltyApi.pointAccounts.expirePoints(tenantId!, active, body);
},
onSuccess: async () => {
toast.success("تراکنش ثبت شد");
setOp(null);
await qc.invalidateQueries({ queryKey: ["loyalty", tenantId, "point-"] });
},
onError: (e: Error) => toast.error(e.message),
});
if (!tenantId || accountsQ.isLoading) return <LoyaltyPageLoader />;
if (accountsQ.error)
return <LoyaltyPageError error={accountsQ.error} onRetry={() => accountsQ.refetch()} />;
const ledgerItems =
ledgerQ.data && "items" in ledgerQ.data ? ledgerQ.data.items : (ledgerQ.data as never) ?? [];
return (
<div>
<LoyaltyBreadcrumbs items={[{ label: "تراکنش امتیاز" }]} />
<PageHeader title="تراکنش امتیاز" description="کسب، مصرف، تعدیل و انقضای امتیاز روی حساب واقعی." />
<div className="mb-4 flex flex-wrap gap-3">
<Select
className="min-w-[260px]"
value={accountId}
onChange={(e) => setAccountId(e.target.value)}
>
<option value="">انتخاب حساب</option>
{(accountsQ.data ?? []).map((a) => (
<option key={a.id} value={a.id}>
{a.account_number} {a.id.slice(0, 8)}
</option>
))}
</Select>
<Button onClick={() => setActive(accountId)}>بارگذاری</Button>
{active ? (
<>
<Button variant="outline" onClick={() => setOp("earn")}>
کسب
</Button>
<Button variant="outline" onClick={() => setOp("redeem")}>
مصرف
</Button>
<Button variant="outline" onClick={() => setOp("adjust")}>
تعدیل
</Button>
<Button variant="outline" onClick={() => setOp("expire")}>
انقضا
</Button>
</>
) : null}
</div>
{balanceQ.data ? (
<p className="mb-4 text-sm">
موجودی: <strong>{balanceQ.data.balance}</strong> {balanceQ.data.currency_name}
</p>
) : null}
{!active ? <EmptyState title="حساب امتیاز را انتخاب کنید" /> : null}
{active && ledgerQ.isLoading ? <LoyaltyPageLoader /> : null}
{active && ledgerQ.data ? (
<LoyaltyTablePage
title="دفترکل امتیاز"
columns={[
{ key: "entry_type", header: "نوع", render: (r) => <LoyaltyStatusChip status={String(r.entry_type)} domain="ledger" /> },
{ key: "amount", header: "مبلغ" },
{ key: "balance_after", header: "مانده" },
{ key: "reason", header: "دلیل" },
{ key: "created_at", header: "زمان" },
]}
rows={ledgerItems as unknown as Record<string, unknown>[]}
/>
) : null}
<Dialog open={!!op} onClose={() => setOp(null)} title="ثبت تراکنش">
<div className="space-y-3">
<FormField label="مبلغ">
<Input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} />
</FormField>
<FormField label="دلیل">
<Input value={reason} onChange={(e) => setReason(e.target.value)} />
</FormField>
<Button loading={mut.isPending} onClick={() => mut.mutate()}>
ثبت
</Button>
</div>
</Dialog>
</div>
);
}