202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import { Copy } from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import { apiGet, apiPatch, apiPost } from "@/lib/api";
|
|
import type { DiscountCode } from "@/lib/types";
|
|
|
|
function copy(s: string) {
|
|
void navigator.clipboard.writeText(s);
|
|
}
|
|
|
|
export function DiscountCodesPage() {
|
|
const { canManageCodes } = useAuth();
|
|
const [rows, setRows] = useState<DiscountCode[]>([]);
|
|
const [open, setOpen] = useState(false);
|
|
const [custom, setCustom] = useState("");
|
|
const [generate, setGenerate] = useState(true);
|
|
const [value, setValue] = useState("10");
|
|
const [dtype, setDtype] = useState<"percent" | "fixed_amount">("percent");
|
|
|
|
const load = useCallback(() => {
|
|
apiGet<{ data: DiscountCode[] }>("/discount-codes").then((r) =>
|
|
setRows(r.data)
|
|
);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
async function create(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
await apiPost("/discount-codes", {
|
|
generate,
|
|
code: generate ? undefined : custom,
|
|
discountType: dtype,
|
|
value: Number(value),
|
|
});
|
|
setOpen(false);
|
|
load();
|
|
}
|
|
|
|
async function toggle(dc: DiscountCode) {
|
|
if (!canManageCodes) return;
|
|
await apiPatch(`/discount-codes/${dc.id}`, { isActive: !dc.isActive });
|
|
load();
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold">Discount codes</h1>
|
|
{canManageCodes && (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button>+ Generate code</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>New discount code</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={create} className="grid gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={generate}
|
|
onChange={(e) => setGenerate(e.target.checked)}
|
|
id="gen"
|
|
/>
|
|
<Label htmlFor="gen">Auto-generate code</Label>
|
|
</div>
|
|
{!generate && (
|
|
<div className="space-y-2">
|
|
<Label>Custom code</Label>
|
|
<Input
|
|
value={custom}
|
|
onChange={(e) => setCustom(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<Label>Type</Label>
|
|
<Select
|
|
value={dtype}
|
|
onValueChange={(v) => setDtype(v as typeof dtype)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="percent">Percent</SelectItem>
|
|
<SelectItem value="fixed_amount">Fixed amount</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Value</Label>
|
|
<Input
|
|
type="number"
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Button type="submit">Create</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
|
|
<Card className="rounded-2xl">
|
|
<CardContent className="pt-6">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Code</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Value</TableHead>
|
|
<TableHead>Redemptions</TableHead>
|
|
<TableHead>Active</TableHead>
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{rows.map((d) => (
|
|
<TableRow key={d.id}>
|
|
<TableCell className="font-mono font-medium">{d.code}</TableCell>
|
|
<TableCell>{d.discountType}</TableCell>
|
|
<TableCell>
|
|
{d.discountType === "percent"
|
|
? `${d.value}%`
|
|
: d.value}
|
|
</TableCell>
|
|
<TableCell>
|
|
{d.redemptionCount}
|
|
{d.maxRedemptions != null && ` / ${d.maxRedemptions}`}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={d.isActive ? "success" : "secondary"}>
|
|
{d.isActive ? "Active" : "Off"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="flex gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
onClick={() => {
|
|
copy(d.code);
|
|
}}
|
|
title="Copy"
|
|
>
|
|
<Copy className="size-4" />
|
|
</Button>
|
|
{canManageCodes && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
type="button"
|
|
onClick={() => toggle(d)}
|
|
>
|
|
Toggle
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|