TorbatYar/frontend/modules/loyalty/features/wallet/wallets.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

130 lines
4.8 KiB
TypeScript
Raw Permalink 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 { z } from "zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button, Dialog, FormField, Input } from "@/components/ds";
import { loyaltyApi } from "@/modules/loyalty/services/loyalty-api";
import { createLoyaltyListPage } from "@/modules/loyalty/components/LoyaltyListCrudPage";
import { LoyaltyStatusChip } from "@/modules/loyalty/design-system";
import type { WalletAccount } from "@/modules/loyalty/types";
import { useTenantId } from "@/hooks/useTenantId";
const createSchema = z.object({
program_id: z.string().uuid(),
member_id: z.string().uuid(),
account_number: z.string().optional(),
currency_code: z.string().default("IRR"),
status: z.enum(["open", "frozen", "closed"]).default("open"),
});
const editSchema = z.object({
status: z.enum(["open", "frozen", "closed"]).optional(),
});
function WalletOps({ id }: { id: string }) {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const [op, setOp] = useState<"credit" | "debit" | "adjust" | null>(null);
const [amount, setAmount] = useState("1000");
const [reason, setReason] = useState("");
const bal = useQuery({
queryKey: ["loyalty", tenantId, "wallet-bal", id],
queryFn: () => loyaltyApi.wallets.balance(tenantId!, id),
enabled: !!tenantId,
});
const m = useMutation({
mutationFn: () => {
const body = { amount: Number(amount), reason: reason || undefined };
if (op === "credit") return loyaltyApi.wallets.credit(tenantId!, id, body);
if (op === "debit") return loyaltyApi.wallets.debit(tenantId!, id, body);
return loyaltyApi.wallets.adjust(tenantId!, id, {
amount: Number(amount),
reason: reason || "adjust",
});
},
onSuccess: () => {
toast.success("تراکنش کیف پول ثبت شد");
setOp(null);
qc.invalidateQueries({ queryKey: ["loyalty", tenantId, "wallet"] });
},
onError: (e: Error) => toast.error(e.message),
});
return (
<div className="flex items-center gap-1">
<span className="text-xs text-[var(--muted)]">{bal.data?.balance ?? "…"}</span>
<Button size="sm" variant="ghost" onClick={() => setOp("credit")}>
+
</Button>
<Button size="sm" variant="ghost" onClick={() => setOp("debit")}>
</Button>
<Button size="sm" variant="ghost" onClick={() => setOp("adjust")}>
±
</Button>
<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={m.isPending} onClick={() => m.mutate()}>
ثبت
</Button>
</div>
</Dialog>
</div>
);
}
export default createLoyaltyListPage<WalletAccount>({
title: "کیف پول وفاداری",
description: "حساب کیف پول اعضا — credit/debit/adjust روی ledger append-only.",
breadcrumb: "کیف پول",
resourceKey: "wallets",
createSchema,
editSchema,
createDefaults: {
program_id: "",
member_id: "",
account_number: "",
currency_code: "IRR",
status: "open",
},
fields: [
{ name: "program_id", label: "برنامه", createOnly: true, required: true },
{ name: "member_id", label: "عضو", createOnly: true, required: true },
{ name: "account_number", label: "شماره", createOnly: true },
{ name: "currency_code", label: "ارز", createOnly: true },
{
name: "status",
label: "وضعیت",
type: "select",
options: [
{ value: "open", label: "باز" },
{ value: "frozen", label: "مسدود" },
{ value: "closed", label: "بسته" },
],
},
],
columns: [
{ key: "account_number", header: "شماره" },
{ key: "member_id", header: "عضو" },
{ key: "currency_code", header: "ارز" },
{
key: "status",
header: "وضعیت",
render: (r) => <LoyaltyStatusChip status={r.status} domain="wallet" />,
},
],
list: (tid) => loyaltyApi.wallets.list(tid, { page: 1, page_size: 500 }),
create: (tid, d) => loyaltyApi.wallets.create(tid, d),
update: (tid, id, d) => loyaltyApi.wallets.update(tid, id, d),
remove: (tid, id) => loyaltyApi.wallets.delete(tid, id),
filterDeleted: true,
exportColumns: ["account_number", "member_id", "currency_code", "status"],
extraActions: (row) => <WalletOps id={row.id} />,
});