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

102 lines
3.7 KiB
TypeScript

"use client";
import { z } from "zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button } 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 { Campaign } from "@/modules/loyalty/types";
import { useTenantId } from "@/hooks/useTenantId";
const createSchema = z.object({
program_id: z.string().uuid(),
code: z.string().min(1),
name: z.string().min(1),
description: z.string().optional(),
status: z.enum(["draft", "scheduled", "active", "paused", "expired"]).default("draft"),
starts_on: z.string().optional(),
ends_on: z.string().optional(),
});
const editSchema = createSchema.partial().omit({ program_id: true, code: true });
function LifecycleButtons({ id }: { id: string }) {
const { tenantId } = useTenantId();
const qc = useQueryClient();
const m = useMutation({
mutationFn: (action: string) => {
const map: Record<string, () => Promise<Campaign>> = {
schedule: () => loyaltyApi.campaigns.schedule(tenantId!, id),
activate: () => loyaltyApi.campaigns.activate(tenantId!, id),
pause: () => loyaltyApi.campaigns.pause(tenantId!, id),
resume: () => loyaltyApi.campaigns.resume(tenantId!, id),
expire: () => loyaltyApi.campaigns.expire(tenantId!, id),
};
return map[action]();
},
onSuccess: () => {
toast.success("وضعیت کمپین به‌روز شد");
qc.invalidateQueries({ queryKey: ["loyalty", tenantId, "campaigns"] });
},
onError: (e: Error) => toast.error(e.message),
});
return (
<div className="flex gap-1">
{["schedule", "activate", "pause", "resume", "expire"].map((a) => (
<Button key={a} size="sm" variant="ghost" onClick={() => m.mutate(a)}>
{a}
</Button>
))}
</div>
);
}
export default createLoyaltyListPage<Campaign>({
title: "کمپین‌ها",
description: "کمپین‌های امتیاز و تعامل — با lifecycle واقعی.",
breadcrumb: "کمپین‌ها",
resourceKey: "campaigns",
createSchema,
editSchema,
createDefaults: {
program_id: "",
code: "",
name: "",
description: "",
status: "draft",
starts_on: "",
ends_on: "",
},
fields: [
{ name: "program_id", label: "برنامه", createOnly: true, required: true },
{ name: "code", label: "کد", createOnly: true, required: true },
{ name: "name", label: "نام", required: true },
{ name: "description", label: "توضیحات", type: "textarea" },
{ name: "starts_on", label: "شروع (YYYY-MM-DD)" },
{ name: "ends_on", label: "پایان (YYYY-MM-DD)" },
],
columns: [
{ key: "code", header: "کد" },
{ key: "name", header: "نام" },
{
key: "status",
header: "وضعیت",
render: (r) => <LoyaltyStatusChip status={r.status} domain="campaign" />,
},
{ key: "rule_version", header: "نسخه قانون" },
],
list: (tid) => loyaltyApi.campaigns.list(tid, { page: 1, page_size: 500 }),
create: (tid, d) => {
const body = { ...d };
if (!body.starts_on) delete body.starts_on;
if (!body.ends_on) delete body.ends_on;
return loyaltyApi.campaigns.create(tid, body);
},
update: (tid, id, d) => loyaltyApi.campaigns.update(tid, id, d),
remove: (tid, id) => loyaltyApi.campaigns.delete(tid, id),
filterDeleted: true,
exportColumns: ["code", "name", "status", "starts_on", "ends_on"],
extraActions: (row) => <LifecycleButtons id={row.id} />,
});