diff --git a/src/actions/admin-vouchers.ts b/src/actions/admin-vouchers.ts index f6c5ddb3..70085ac3 100644 --- a/src/actions/admin-vouchers.ts +++ b/src/actions/admin-vouchers.ts @@ -1,30 +1,42 @@ "use server"; import { revalidatePath } from "next/cache"; +import { positiveBigInt } from "@/lib/api"; import { requirePermission } from "@/lib/admin/guard"; import { PERMS } from "@/lib/permissions"; -import { formPositiveBigInt } from "@/lib/form-data"; import { prisma } from "@/lib/prisma"; +import { + actionError, + actionOk, + type ActionResult, +} from "@/lib/safe-action-shared"; import { logServerError } from "@/lib/server-log"; -export async function createVoucher(formData: FormData): Promise { +export async function createVoucher(input: { + code: string; + amount: number; + maxUses: number; + expiresAt?: string; +}): Promise> { await requirePermission(PERMS.SHOP_EDIT); - const code = String(formData.get("code") ?? "") + const code = String(input.code ?? "") .normalize("NFC") .trim() .slice(0, 255); - const amount = Number(formData.get("amount")); - const maxUsesRaw = Number(formData.get("maxUses")); + const amount = Number(input.amount); + const maxUsesRaw = Number(input.maxUses); const maxUses = Number.isFinite(maxUsesRaw) && maxUsesRaw > 0 ? Math.floor(maxUsesRaw) : 1; - if (!code || !(amount > 0)) return; + if (!code || !(amount > 0)) { + return actionError("Code and a positive amount are required"); + } - const expiresRaw = String(formData.get("expiresAt") ?? "") + let expiresAt: Date | null = null; + const expiresRaw = String(input.expiresAt ?? "") .normalize("NFC") .trim(); - let expiresAt: Date | null = null; if (expiresRaw) { const parsed = new Date(expiresRaw); if (!Number.isNaN(parsed.getTime())) expiresAt = parsed; @@ -33,7 +45,7 @@ export async function createVoucher(formData: FormData): Promise { const now = new Date(); try { - await prisma.websiteShopVouchers.create({ + const created = await prisma.websiteShopVouchers.create({ data: { code, amount: Math.floor(amount), @@ -44,29 +56,30 @@ export async function createVoucher(formData: FormData): Promise { updatedAt: now, }, }); + revalidatePath("/admin/vouchers"); + return actionOk({ id: String(created.id) }); } catch (error) { logServerError("admin.voucher_create_failed", error); - // Unique constraint on `code` (or DB unavailable) — swallow and re-render. - return; + return actionError("Could not create voucher (code may already exist)"); } - - revalidatePath("/admin/vouchers"); } -export async function deleteVoucher(formData: FormData): Promise { +export async function deleteVoucher(input: { + id: string; +}): Promise { await requirePermission(PERMS.SHOP_EDIT); - const id = formPositiveBigInt(formData, "id"); - if (!id) return; + const id = positiveBigInt(String(input.id ?? "").trim()); + if (!id) return actionError("Missing voucher id"); try { await prisma.websiteShopVouchers.delete({ where: { id } }); + revalidatePath("/admin/vouchers"); + return actionOk(); } catch (error) { logServerError("admin.voucher_delete_failed", error, { voucherId: String(id), }); - return; + return actionError("Could not delete voucher"); } - - revalidatePath("/admin/vouchers"); } diff --git a/src/app/admin/shop/page.tsx b/src/app/admin/shop/page.tsx index 77d0aa9e..1f2188e0 100644 --- a/src/app/admin/shop/page.tsx +++ b/src/app/admin/shop/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { StatusCard } from "@/components/admin/dashboard"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; @@ -26,6 +27,24 @@ function statusState( return undefined; } +function CurrencyCell({ + kind, + value, +}: { + kind: "credits" | "duckets" | "diamonds"; + value: number | null | undefined; +}) { + if (value == null) { + return —; + } + return ( + + + {value} + + ); +} + export default async function AdminShop() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) { @@ -64,7 +83,7 @@ export default async function AdminShop() { return (
-
+
{t("newPackage")} @@ -82,13 +101,13 @@ export default async function AdminShop() {
-

+

{t("packages")} ({packages.length})

{packages.length === 0 ? ( -

{t("noPackages")}

+
{t("noPackages")}
) : ( -
+
@@ -110,9 +129,15 @@ export default async function AdminShop() { {p.name} - - - + + + @@ -124,16 +149,14 @@ export default async function AdminShop() {
-

+

{t("recentOrders")} ({orders.length})

-

- {t("ordersHint")} (website_paypal_transactions). -

+

{t("ordersHint")}

{orders.length === 0 ? ( -

{t("noOrders")}

+
{t("noOrders")}
) : ( -
+
{p.costs}{p.credits ?? "—"}{p.duckets ?? "—"}{p.diamonds ?? "—"} + + + + + + {p.giveRank ?? "—"} {p.position}
@@ -153,7 +176,11 @@ export default async function AdminShop() { - +
{o.transactionId} {o.userId} + + #{o.userId} + + {o.description ?? "—"} {o.amount} {o.currency} diff --git a/src/app/admin/transactions/page.tsx b/src/app/admin/transactions/page.tsx index 5c1e5fc9..8f2ec596 100644 --- a/src/app/admin/transactions/page.tsx +++ b/src/app/admin/transactions/page.tsx @@ -1,35 +1,35 @@ -import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { StatusCard } from "@/components/admin/dashboard"; -import { prisma } from "@/lib/prisma"; import { redirect } from "next/navigation"; +import type { Prisma } from "@/generated/prisma/client"; +import { StatusCard } from "@/components/admin/dashboard"; +import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; +import { prisma } from "@/lib/prisma"; +import { + TransactionsTable, + type TransactionRow, +} from "./transactions-table"; export const dynamic = "force-dynamic"; -const PER_PAGE = 25; - -type Txn = Awaited< - ReturnType ->[number]; - -const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]); -const FAILED_STATUS = new Set([ +const PAID_STATUS = ["completed", "paid", "success", "approved"] as const; +const FAILED_STATUS = [ "failed", "denied", "refunded", "cancelled", "canceled", -]); +] as const; +const PENDING_STATUS = ["pending", "created", "processing"] as const; -function statusState( +function statusKind( status: string | null | undefined, -): "ok" | "danger" | undefined { - if (!status) return undefined; +): TransactionRow["statusKind"] { + if (!status) return "neutral"; const s = status.toLowerCase(); - if (PAID_STATUS.has(s)) return "ok"; - if (FAILED_STATUS.has(s)) return "danger"; - return undefined; + if ((PAID_STATUS as readonly string[]).includes(s)) return "ok"; + if ((FAILED_STATUS as readonly string[]).includes(s)) return "danger"; + return "neutral"; } function fromDate(d: Date | null | undefined): string { @@ -42,10 +42,22 @@ function formatAmount(amount: number, currency: string): string { return `${value} ${currency}`; } +const SORT_MAP: Record< + string, + Prisma.WebsitePaypalTransactionsOrderByWithRelationInput +> = { + id: { id: "desc" }, + amount: { amount: "desc" }, + date: { createdAt: "desc" }, + status: { status: "desc" }, + package: { description: "desc" }, + payer: { userId: "desc" }, +}; + export default async function AdminTransactions({ searchParams, }: { - searchParams: Promise<{ page?: string }>; + searchParams: Promise>; }) { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) { @@ -53,46 +65,106 @@ export default async function AdminTransactions({ } const t = await getTranslations("pages.admin.transactions"); + const raw = await searchParams; + const parsed = parseListParams(new URLSearchParams(raw)); + const statusFilter = raw.filter_status ?? ""; - const sp = await searchParams; - const page = Math.max(1, Number(sp.page ?? "1") || 1); + const where: Prisma.WebsitePaypalTransactionsWhereInput = {}; + + if (statusFilter === "paid") { + where.status = { in: [...PAID_STATUS] }; + } else if (statusFilter === "failed") { + where.status = { in: [...FAILED_STATUS] }; + } else if (statusFilter === "pending") { + where.status = { in: [...PENDING_STATUS] }; + } + + if (parsed.search.trim()) { + const q = parsed.search.trim(); + const asId = Number(q); + let userIds: number[] = []; + try { + const users = await prisma.user.findMany({ + where: { username: { contains: q } }, + select: { id: true }, + take: 50, + }); + userIds = users.map((u) => u.id); + } catch { + userIds = []; + } + + where.AND = [ + ...(where.status ? [{ status: where.status }] : []), + { + OR: [ + { transactionId: { contains: q } }, + { description: { contains: q } }, + ...(Number.isFinite(asId) && asId > 0 ? [{ userId: asId }] : []), + ...(userIds.length ? [{ userId: { in: userIds } }] : []), + ], + }, + ]; + delete where.status; + } - let transactions: Txn[] = []; let total = 0; - let names = new Map(); - try { - [transactions, total] = await Promise.all([ - prisma.websitePaypalTransactions.findMany({ - orderBy: { id: "desc" }, - skip: (page - 1) * PER_PAGE, - take: PER_PAGE, - }), - prisma.websitePaypalTransactions.count(), - ]); + total = await prisma.websitePaypalTransactions.count({ where }); + } catch { + total = 0; + } - const userIds = [...new Set(transactions.map((t) => t.userId))]; - if (userIds.length > 0) { + const pagination = calcPagination(total, parsed.page, parsed.perPage); + const sortKey = parsed.sort ?? "id"; + const baseOrder = SORT_MAP[sortKey] ?? { id: "desc" as const }; + const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; + const orderBy = { + [orderField]: parsed.order, + } as Prisma.WebsitePaypalTransactionsOrderByWithRelationInput; + + let transactions: Awaited< + ReturnType + > = []; + try { + transactions = await prisma.websitePaypalTransactions.findMany({ + where, + orderBy, + skip: pagination.offset, + take: pagination.perPage, + }); + } catch { + transactions = []; + } + + const userIds = [...new Set(transactions.map((x) => x.userId))]; + let names = new Map(); + if (userIds.length > 0) { + try { const users = await prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, username: true }, }); names = new Map(users.map((u) => [u.id, u.username])); + } catch { + names = new Map(); } - } catch { - transactions = []; - total = 0; - names = new Map(); } - const pages = Math.max(1, Math.ceil(total / PER_PAGE)); + const rows: TransactionRow[] = transactions.map((x) => ({ + id: Number(x.id), + payer: names.get(x.userId) ?? `#${x.userId}`, + payerId: x.userId, + amount: formatAmount(x.amount, x.currency), + package: x.description ?? "—", + status: x.status ?? "", + statusKind: statusKind(x.status), + transactionId: x.transactionId, + date: fromDate(x.createdAt), + })); - const pagePaid = transactions.filter( - (t) => statusState(t.status) === "ok", - ).length; - const pageFailed = transactions.filter( - (t) => statusState(t.status) === "danger", - ).length; + const pagePaid = rows.filter((r) => r.statusKind === "ok").length; + const pageFailed = rows.filter((r) => r.statusKind === "danger").length; return (
@@ -113,87 +185,21 @@ export default async function AdminTransactions({
-

- {t("title")} ({total}) -

- {transactions.length === 0 ? ( +

{t("title")}

+ {total === 0 && !parsed.search && !statusFilter ? (
{t("noTransactions")}
) : ( -
- - - - - - - - - - - - - - {transactions.map((x) => { - const state = statusState(x.status); - return ( - - - - - - - - - - ); - })} - -
{t("colId")}{t("colPayer")}{t("colAmount")}{t("colPackage")}{t("colStatus")}{t("colTransaction")}{t("colDate")}
- {String(x.id)} - - - {names.get(x.userId) ?? `#${x.userId}`} - - {formatAmount(x.amount, x.currency)}{x.description ?? "—"} - {x.status ? ( - - {x.status} - - ) : ( - - — - - )} - - {x.transactionId} - - {fromDate(x.createdAt)} -
-
+ )}
- -

- {t("pagination", { page, pages, total })} - {page > 1 ? ( - <> - {" · "} - - ← {t("prev")} - - - ) : null} - {page < pages ? ( - <> - {" · "} - - {t("next")} → - - - ) : null} -

); } diff --git a/src/app/admin/transactions/transactions-table.tsx b/src/app/admin/transactions/transactions-table.tsx new file mode 100644 index 00000000..0d8f2178 --- /dev/null +++ b/src/app/admin/transactions/transactions-table.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { DataTable } from "@/components/admin/data-table"; +import { Badge } from "@/components/ui/badge"; +import type { DataTableColumn, PaginatedResult } from "@/types"; + +export interface TransactionRow { + id: number; + payer: string; + payerId: number; + amount: string; + package: string; + status: string; + statusKind: "ok" | "danger" | "neutral"; + transactionId: string; + date: string; +} + +function statusVariant( + kind: TransactionRow["statusKind"], +): "default" | "destructive" | "secondary" { + if (kind === "ok") return "default"; + if (kind === "danger") return "destructive"; + return "secondary"; +} + +export function TransactionsTable({ + data, +}: { + data: PaginatedResult; +}) { + const t = useTranslations("pages.admin.transactions"); + + const columns: DataTableColumn[] = [ + { key: "id", label: t("colId"), sortable: true }, + { + key: "payer", + label: t("colPayer"), + sortable: true, + render: (_value, row) => ( + + {row.payer} + + ), + }, + { key: "amount", label: t("colAmount"), sortable: true }, + { key: "package", label: t("colPackage"), sortable: true }, + { + key: "status", + label: t("colStatus"), + sortable: true, + filterKey: "filter_status", + filterOptions: [ + { label: t("statusCompleted"), value: "paid" }, + { label: t("statusPending"), value: "pending" }, + { label: t("statusFailed"), value: "failed" }, + ], + render: (_value, row) => + row.status ? ( + + {row.status} + + ) : ( + — + ), + }, + { key: "transactionId", label: t("colTransaction") }, + { key: "date", label: t("colDate"), sortable: true }, + ]; + + return ( + + ); +} diff --git a/src/app/admin/vouchers/page.tsx b/src/app/admin/vouchers/page.tsx index 5a4c7831..b73586d1 100644 --- a/src/app/admin/vouchers/page.tsx +++ b/src/app/admin/vouchers/page.tsx @@ -1,39 +1,22 @@ -import { getTranslations } from "next-intl/server"; -import { createVoucher, deleteVoucher } from "@/actions/admin-vouchers"; -import { StatusCard } from "@/components/admin/dashboard"; -import { prisma } from "@/lib/prisma"; import { redirect } from "next/navigation"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; +import { prisma } from "@/lib/prisma"; +import { VouchersClient, type VoucherRow } from "./vouchers-client"; export const dynamic = "force-dynamic"; -type VoucherRow = { - id: bigint; - code: string; - amount: number; - maxUses: number; - useCount: number; - expiresAt: Date | null; - createdAt: Date | null; -}; - -function fmtDate(d: Date | null): string { - if (!d) return "—"; - return d.toISOString().slice(0, 16).replace("T", " "); -} - export default async function AdminVouchers() { const { session, permissions } = await getAdminContext(); if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) { redirect("/admin"); } - const t = await getTranslations("pages.admin.vouchers"); + const canEdit = canAccess(permissions, PERMS.SHOP_EDIT, session.user.rank); const now = Date.now(); - let vouchers: VoucherRow[] = []; + let rows: VoucherRow[] = []; try { - vouchers = await prisma.websiteShopVouchers.findMany({ + const vouchers = await prisma.websiteShopVouchers.findMany({ select: { id: true, code: true, @@ -46,182 +29,37 @@ export default async function AdminVouchers() { orderBy: { id: "desc" }, take: 200, }); + rows = vouchers.map((v) => { + const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false; + const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses; + const status: VoucherRow["status"] = expired + ? "expired" + : usedUp + ? "usedUp" + : "active"; + return { + id: String(v.id), + code: v.code, + amount: v.amount, + maxUses: v.maxUses, + useCount: v.useCount, + expiresAt: v.expiresAt?.toISOString() ?? null, + createdAt: v.createdAt?.toISOString() ?? null, + status, + }; + }); } catch { - vouchers = []; + rows = []; } - const active = vouchers.filter((v) => { - const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false; - const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses; - return !expired && !usedUp; - }).length; - const expiredCount = vouchers.filter( - (v) => v.expiresAt && v.expiresAt.getTime() <= now, - ).length; - const usedUpCount = vouchers.filter( - (v) => v.maxUses > 0 && v.useCount >= v.maxUses, - ).length; + const stats = { + total: rows.length, + active: rows.filter((v) => v.status === "active").length, + usedUp: rows.filter((v) => v.status === "usedUp").length, + expired: rows.filter((v) => v.status === "expired").length, + }; return ( -
-
- - 0 ? "ok" : "neutral"} - icon="✅" - /> - 0 ? "warn" : "neutral"} - icon="📉" - /> - 0 ? "danger" : "neutral"} - icon="⏰" - /> -
- -
-

{t("newVoucher")}

-
- - - - -
- -
- -
-

- {t("title")} ({vouchers.length}) -

-
- - - - - - - - - - - - - {vouchers.map((v) => { - const expired = v.expiresAt - ? v.expiresAt.getTime() <= now - : false; - const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses; - return ( - - - - - - - - - - ); - })} - -
{t("colCode")}{t("colAmount")}{t("colUses")}{t("colStatus")}{t("colExpires")}{t("colCreated")} -
- {v.code} - {v.amount} - {v.useCount} / {v.maxUses} - - {expired ? ( - - {t("statusExpired")} - - ) : usedUp ? ( - - {t("statusUsedUp")} - - ) : ( - - {t("statusActive")} - - )} - - {fmtDate(v.expiresAt)} - - {fmtDate(v.createdAt)} - -
- - -
-
- {vouchers.length === 0 ? ( -
{t("noVouchers")}
- ) : null} -
-
-
+ ); } diff --git a/src/app/admin/vouchers/vouchers-client.tsx b/src/app/admin/vouchers/vouchers-client.tsx new file mode 100644 index 00000000..bd8916a5 --- /dev/null +++ b/src/app/admin/vouchers/vouchers-client.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { createVoucher, deleteVoucher } from "@/actions/admin-vouchers"; +import { ConfirmDialog } from "@/components/admin/confirm-dialog"; +import { StatusCard } from "@/components/admin/dashboard"; +import { CurrencyIcon } from "@/components/shared/currency-icon"; +import { useServerAction } from "@/hooks/use-server-action"; + +export type VoucherRow = { + id: string; + code: string; + amount: number; + maxUses: number; + useCount: number; + expiresAt: string | null; + createdAt: string | null; + status: "active" | "expired" | "usedUp"; +}; + +function fmtDate(iso: string | null): string { + if (!iso) return "—"; + return iso.slice(0, 16).replace("T", " "); +} + +export function VouchersClient({ + vouchers, + canEdit, + stats, +}: { + vouchers: VoucherRow[]; + canEdit: boolean; + stats: { total: number; active: number; usedUp: number; expired: number }; +}) { + const t = useTranslations("pages.admin.vouchers"); + const { run, isPending } = useServerAction(); + const [pendingDelete, setPendingDelete] = useState(null); + const [code, setCode] = useState(""); + const [amount, setAmount] = useState(""); + const [maxUses, setMaxUses] = useState("1"); + const [expiresAt, setExpiresAt] = useState(""); + + function handleCreate(e: React.FormEvent) { + e.preventDefault(); + if (!canEdit) return; + run( + () => + createVoucher({ + code, + amount: Number(amount), + maxUses: Number(maxUses), + expiresAt: expiresAt || undefined, + }), + { + successMessage: t("created"), + errorMessage: t("createError"), + onSuccess: () => { + setCode(""); + setAmount(""); + setMaxUses("1"); + setExpiresAt(""); + }, + }, + ); + } + + function handleConfirmDelete() { + if (!pendingDelete) return; + const id = pendingDelete.id; + run(() => deleteVoucher({ id }), { + successMessage: t("deleted"), + errorMessage: t("deleteError"), + onSuccess: () => setPendingDelete(null), + }); + } + + return ( +
+
+ + 0 ? "ok" : "neutral"} + icon="✅" + /> + 0 ? "warn" : "neutral"} + icon="📉" + /> + 0 ? "danger" : "neutral"} + icon="⏰" + /> +
+ + {canEdit && ( +
+

{t("newVoucher")}

+
+ + + + +
+ +
+ )} + +
+

+ {t("title")} ({vouchers.length}) +

+
+ {vouchers.length === 0 ? ( +
{t("noVouchers")}
+ ) : ( + + + + + + + + + + {canEdit && } + + + + {vouchers.map((v) => ( + + + + + + + + {canEdit && ( + + )} + + ))} + +
{t("colCode")}{t("colAmount")}{t("colUses")}{t("colStatus")}{t("colExpires")}{t("colCreated")}{t("colActions")}
+ {v.code} + + + + {v.amount} + + + {v.useCount} / {v.maxUses} + + {v.status === "expired" ? ( + + {t("statusExpired")} + + ) : v.status === "usedUp" ? ( + + {t("statusUsedUp")} + + ) : ( + + {t("statusActive")} + + )} + + {fmtDate(v.expiresAt)} + + {fmtDate(v.createdAt)} + + +
+ )} +
+
+ + { + if (!open) setPendingDelete(null); + }} + title={t("confirmDeleteTitle")} + description={ + pendingDelete + ? t("confirmDelete", { code: pendingDelete.code }) + : "" + } + confirmLabel={t("delete")} + cancelLabel={t("form.cancel")} + loading={isPending} + onConfirm={handleConfirmDelete} + /> +
+ ); +} diff --git a/src/messages/en.json b/src/messages/en.json index 433e78fb..f718ffb5 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -1229,6 +1229,9 @@ "subtitle": "Manage packages and orders", "packages": "Packages", "orders": "Orders", + "recentOrders": "Recent orders", + "paidOrders": "Paid orders", + "ordersHint": "Latest PayPal transactions", "newPackage": "+ New package", "edit": "Edit", "delete": "Delete", @@ -1239,6 +1242,11 @@ "colId": "ID", "colName": "Name", "colPrice": "Price", + "colCredits": "Credits", + "colDuckets": "Duckets", + "colDiamonds": "Diamonds", + "colRank": "Rank", + "colPosition": "Position", "colCurrency": "Currency", "colType": "Type", "colSold": "Sold", @@ -1247,6 +1255,9 @@ "colOrderId": "Order ID", "colCustomer": "Customer", "colPackage": "Package", + "colTransaction": "Transaction", + "colUser": "User", + "colDescription": "Description", "colAmount": "Amount", "colStatus": "Status", "colDate": "Date", @@ -1283,12 +1294,20 @@ "title": "Transactions", "subtitle": "PayPal payment log", "noTransactions": "No transactions yet", + "totalTransactions": "Total transactions", + "pagePaid": "Paid (this page)", + "pageFailed": "Failed (this page)", + "searchPlaceholder": "Search by user, transaction ID or package…", + "presetPaid": "Paid", + "presetFailed": "Failed", "colId": "ID", "colUser": "User", + "colPayer": "Payer", "colAmount": "Amount", "colCurrency": "Currency", "colPackage": "Package", "colStatus": "Status", + "colTransaction": "Transaction ID", "colDate": "Date", "colActions": "Actions", "statusCompleted": "Completed", @@ -1307,16 +1326,30 @@ "vouchers": { "title": "Shop Vouchers", "subtitle": "Manage redeemable voucher codes", + "vouchers": "Vouchers", + "active": "Active", + "usedUp": "Used up", + "expired": "Expired", "newVoucher": "+ New voucher", "edit": "Edit", "delete": "Delete", "noVouchers": "No vouchers yet", + "confirmDeleteTitle": "Delete voucher", "confirmDelete": "Delete voucher \"{code}\"?", + "created": "Voucher created", + "deleted": "Voucher deleted", + "createError": "Could not create voucher", + "deleteError": "Could not delete voucher", + "statusActive": "Active", + "statusUsedUp": "Used up", + "statusExpired": "Expired", "colId": "ID", "colCode": "Code", "colValue": "Value", + "colAmount": "Amount", "colType": "Type", "colUses": "Uses / Max", + "colStatus": "Status", "colExpires": "Expires", "colCreated": "Created", "colActions": "Actions", @@ -1324,6 +1357,8 @@ "code": "Code", "codePlaceholder": "e.g. SUMMER2025", "value": "Value", + "amount": "Credits amount", + "amountPlaceholder": "e.g. 100", "type": "Type", "typeCredits": "Credits", "typeDuckets": "Duckets", @@ -1338,6 +1373,7 @@ "maxUsesPlaceholder": "0 = unlimited", "expiresAt": "Expires at", "enabled": "Enabled", + "create": "Create voucher", "save": "Save voucher", "saving": "Saving…", "cancel": "Cancel" diff --git a/src/messages/it.json b/src/messages/it.json index 64017425..aa0b1e79 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -1193,6 +1193,9 @@ "subtitle": "Gestisci pacchetti e ordini", "packages": "Pacchetti", "orders": "Ordini", + "recentOrders": "Ordini recenti", + "paidOrders": "Ordini pagati", + "ordersHint": "Ultime transazioni PayPal", "newPackage": "+ Nuovo pacchetto", "edit": "Modifica", "delete": "Elimina", @@ -1203,6 +1206,11 @@ "colId": "ID", "colName": "Nome", "colPrice": "Prezzo", + "colCredits": "Crediti", + "colDuckets": "Duckets", + "colDiamonds": "Diamanti", + "colRank": "Grado", + "colPosition": "Posizione", "colCurrency": "Valuta", "colType": "Tipo", "colSold": "Venduti", @@ -1211,6 +1219,9 @@ "colOrderId": "ID ordine", "colCustomer": "Cliente", "colPackage": "Pacchetto", + "colTransaction": "Transazione", + "colUser": "Utente", + "colDescription": "Descrizione", "colAmount": "Importo", "colStatus": "Stato", "colDate": "Data", @@ -1247,12 +1258,20 @@ "title": "Transazioni", "subtitle": "Registro pagamenti PayPal", "noTransactions": "Ancora nessuna transazione", + "totalTransactions": "Transazioni totali", + "pagePaid": "Pagate (questa pagina)", + "pageFailed": "Fallite (questa pagina)", + "searchPlaceholder": "Cerca per utente, ID transazione o pacchetto…", + "presetPaid": "Pagate", + "presetFailed": "Fallite", "colId": "ID", "colUser": "Utente", + "colPayer": "Pagatore", "colAmount": "Importo", "colCurrency": "Valuta", "colPackage": "Pacchetto", "colStatus": "Stato", + "colTransaction": "ID transazione", "colDate": "Data", "colActions": "Azioni", "statusCompleted": "Completato", @@ -1271,16 +1290,30 @@ "vouchers": { "title": "Buoni negozio", "subtitle": "Gestisci codici buono riscattabili", + "vouchers": "Buoni", + "active": "Attivi", + "usedUp": "Esauriti", + "expired": "Scaduti", "newVoucher": "+ Nuovo buono", "edit": "Modifica", "delete": "Elimina", "noVouchers": "Ancora nessun buono", + "confirmDeleteTitle": "Elimina buono", "confirmDelete": "Eliminare il buono \"{code}\"?", + "created": "Buono creato", + "deleted": "Buono eliminato", + "createError": "Impossibile creare il buono", + "deleteError": "Impossibile eliminare il buono", + "statusActive": "Attivo", + "statusUsedUp": "Esaurito", + "statusExpired": "Scaduto", "colId": "ID", "colCode": "Codice", "colValue": "Valore", + "colAmount": "Importo", "colType": "Tipo", "colUses": "Usi / Max", + "colStatus": "Stato", "colExpires": "Scade", "colCreated": "Creato", "colActions": "Azioni", @@ -1288,6 +1321,8 @@ "code": "Codice", "codePlaceholder": "es. ESTATE2025", "value": "Valore", + "amount": "Crediti", + "amountPlaceholder": "es. 100", "type": "Tipo", "typeCredits": "Crediti", "typeDuckets": "Duckets", @@ -1302,6 +1337,7 @@ "maxUsesPlaceholder": "0 = illimitato", "expiresAt": "Scade il", "enabled": "Attivo", + "create": "Crea buono", "save": "Salva buono", "saving": "Salvataggio…", "cancel": "Annulla"