hi-frontend/apps/admin/src/sections/redemption/redemption-records.tsx
EUForest 585874777f feat: improve redemption feature i18n and error code handling
- Complete internationalization for redemption records dialog
  - Add i18n for table headers and unit time translations
  - Optimize redemption form validation error messages with i18n
  - Add 50003 error code handling for coupon plan incompatibility
  - Fix TypeScript errors in redemption-related components
  - Fix TypeScript errors in user announcement page
2026-01-07 15:38:42 +08:00

151 lines
5.2 KiB
TypeScript

import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@workspace/ui/components/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@workspace/ui/components/table";
import { getRedemptionRecordList } from "@workspace/ui/services/admin/redemption";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/utils/common";
interface RedemptionRecordsProps {
codeId: number | null;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export default function RedemptionRecords({
codeId,
open,
onOpenChange,
}: RedemptionRecordsProps) {
const { t } = useTranslation("redemption");
const [loading, setLoading] = useState(false);
const [records, setRecords] = useState<API.RedemptionRecord[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState({ page: 1, size: 10 });
const fetchRecords = async () => {
if (!codeId) return;
setLoading(true);
try {
const { data } = await getRedemptionRecordList({
...pagination,
code_id: codeId,
});
setRecords(data.data?.list || []);
setTotal(data.data?.total || 0);
} catch (error) {
console.error("Failed to fetch redemption records:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open && codeId) {
fetchRecords();
}
}, [open, codeId, pagination]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{t("records", "Redemption Records")}
</DialogTitle>
</DialogHeader>
<div className="mt-4">
{loading ? (
<div className="flex justify-center py-8">
<span>{t("loading", "Loading...")}</span>
</div>
) : records.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{t("noRecords", "No records found")}
</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("id", "ID")}</TableHead>
<TableHead>{t("userId", "User ID")}</TableHead>
<TableHead>{t("subscribeId", "Subscribe ID")}</TableHead>
<TableHead>{t("unitTime", "Unit Time")}</TableHead>
<TableHead>{t("quantity", "Quantity")}</TableHead>
<TableHead>{t("redeemedAt", "Redeemed At")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{records.map((record) => {
const unitTimeMap: Record<string, string> = {
day: t("form.day", "Day"),
month: t("form.month", "Month"),
quarter: t("form.quarter", "Quarter"),
half_year: t("form.halfYear", "Half Year"),
year: t("form.year", "Year"),
};
return (
<TableRow key={record.id}>
<TableCell>{record.id}</TableCell>
<TableCell>{record.user_id}</TableCell>
<TableCell>{record.subscribe_id}</TableCell>
<TableCell>{unitTimeMap[record.unit_time] || record.unit_time}</TableCell>
<TableCell>{record.quantity}</TableCell>
<TableCell>
{record.redeemed_at
? formatDate(record.redeemed_at)
: "--"}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{total > pagination.size && (
<div className="flex justify-between items-center mt-4">
<span className="text-sm text-muted-foreground">
{t("total", "Total")}: {total}
</span>
<div className="flex gap-2">
<button
className="px-3 py-1 text-sm border rounded hover:bg-accent disabled:opacity-50"
disabled={pagination.page === 1}
onClick={() =>
setPagination((p) => ({ ...p, page: p.page - 1 }))
}
>
{t("previous", "Previous")}
</button>
<button
className="px-3 py-1 text-sm border rounded hover:bg-accent disabled:opacity-50"
disabled={pagination.page * pagination.size >= total}
onClick={() =>
setPagination((p) => ({ ...p, page: p.page + 1 }))
}
>
{t("next", "Next")}
</button>
</div>
</div>
)}
</>
)}
</div>
</DialogContent>
</Dialog>
);
}