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>
117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
LoadingState,
|
|
ErrorState,
|
|
Badge,
|
|
EmptyState,
|
|
} from "@/components/ds";
|
|
import { loyaltyApi, LoyaltyApiError } from "@/modules/loyalty/services/loyalty-api";
|
|
import { useTenantId } from "@/hooks/useTenantId";
|
|
import { LoyaltyStatGrid } from "@/modules/loyalty/design-system";
|
|
|
|
export function LoyaltyPageLoader({ label = "در حال بارگذاری…" }: { label?: string }) {
|
|
return <LoadingState label={label} />;
|
|
}
|
|
|
|
export function LoyaltyPageError({
|
|
error,
|
|
onRetry,
|
|
}: {
|
|
error: unknown;
|
|
onRetry?: () => void;
|
|
}) {
|
|
if (error instanceof LoyaltyApiError && error.status === 403) {
|
|
return (
|
|
<EmptyState
|
|
title="دسترسی محدود"
|
|
description={error.message || "مجوز لازم برای این عملیات وجود ندارد."}
|
|
/>
|
|
);
|
|
}
|
|
const message = error instanceof Error ? error.message : "خطا در دریافت دادهها";
|
|
return <ErrorState message={message} onRetry={onRetry} />;
|
|
}
|
|
|
|
export function useLoyaltyHealth() {
|
|
return useQuery({
|
|
queryKey: ["loyalty", "health"],
|
|
queryFn: () => loyaltyApi.health(),
|
|
});
|
|
}
|
|
|
|
export function LoyaltyServiceBadge() {
|
|
const healthQ = useLoyaltyHealth();
|
|
if (!healthQ.data) return null;
|
|
return (
|
|
<Badge tone={healthQ.data.status === "ok" ? "success" : "danger"}>
|
|
Loyalty v{healthQ.data.version}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
export function useLoyaltyCounts(tenantId: string | null) {
|
|
return useQuery({
|
|
queryKey: ["loyalty", tenantId, "counts"],
|
|
queryFn: async () => {
|
|
const [programs, members, tiers, rewards, campaigns, wallets, pointAccounts] =
|
|
await Promise.all([
|
|
loyaltyApi.programs.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.members.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.tiers.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.rewards.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.campaigns.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.wallets.list(tenantId!, { page: 1, page_size: 500 }),
|
|
loyaltyApi.pointAccounts.list(tenantId!, { page: 1, page_size: 500 }),
|
|
]);
|
|
return {
|
|
programs: programs.length,
|
|
members: members.length,
|
|
activeMembers: members.filter((m) => m.status === "active").length,
|
|
tiers: tiers.length,
|
|
rewards: rewards.length,
|
|
campaigns: campaigns.length,
|
|
activeCampaigns: campaigns.filter((c) => c.status === "active").length,
|
|
wallets: wallets.length,
|
|
pointAccounts: pointAccounts.length,
|
|
};
|
|
},
|
|
enabled: !!tenantId,
|
|
});
|
|
}
|
|
|
|
export function LoyaltyCountsGrid({ tenantId }: { tenantId: string }) {
|
|
const q = useLoyaltyCounts(tenantId);
|
|
if (q.isLoading) return <LoyaltyPageLoader />;
|
|
if (q.error) return <LoyaltyPageError error={q.error} onRetry={() => q.refetch()} />;
|
|
const c = q.data!;
|
|
return (
|
|
<LoyaltyStatGrid
|
|
stats={[
|
|
{ label: "برنامهها", value: c.programs },
|
|
{ label: "اعضا", value: c.members, hint: `${c.activeMembers} فعال` },
|
|
{ label: "سطوح", value: c.tiers },
|
|
{ label: "پاداشها", value: c.rewards },
|
|
{ label: "کمپینها", value: c.campaigns, hint: `${c.activeCampaigns} فعال` },
|
|
{ label: "کیف پول", value: c.wallets },
|
|
{ label: "حساب امتیاز", value: c.pointAccounts },
|
|
]}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function exportToCsv(filename: string, rows: Record<string, unknown>[], columns: string[]) {
|
|
const header = columns.join(",");
|
|
const lines = rows.map((r) => columns.map((c) => JSON.stringify(r[c] ?? "")).join(","));
|
|
const blob = new Blob(["\uFEFF" + [header, ...lines].join("\n")], {
|
|
type: "text/csv;charset=utf-8;",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|