206 lines
6.5 KiB
TypeScript
206 lines
6.5 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 { useAuthStore } from "@/store/authStore";
|
|
import { apiGet, apiPatch, apiPost } from "@/lib/api";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import type { DiscountCode } from "@/lib/types";
|
|
|
|
function copy(s: string) {
|
|
void navigator.clipboard.writeText(s);
|
|
}
|
|
|
|
export function DiscountCodesPage() {
|
|
const { canManageCodes } = useAuth();
|
|
const selectedPropertyId = useAuthStore((s) => s.selectedPropertyId);
|
|
const [rows, setRows] = useState<DiscountCode[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
const [custom, setCustom] = useState("");
|
|
const [value, setValue] = useState("10");
|
|
const [dtype, setDtype] = useState<"percent" | "fixed_amount">("percent");
|
|
|
|
const load = useCallback(() => {
|
|
setLoading(true);
|
|
apiGet<{ data: DiscountCode[] }>("/discount-codes").then((r) =>
|
|
setRows(r.data)
|
|
).finally(() => setLoading(false));
|
|
}, [selectedPropertyId]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
async function create(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setSubmitting(true);
|
|
try {
|
|
await apiPost("/discount-codes", {
|
|
code: custom,
|
|
discountType: dtype,
|
|
value: Number(value),
|
|
});
|
|
setOpen(false);
|
|
load();
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
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="space-y-2">
|
|
<Label>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" loading={submitting}>Create</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)}
|
|
</div>
|
|
|
|
<Card className="rounded-2xl">
|
|
<CardContent className="pt-6">
|
|
{loading && rows.length === 0 ? (
|
|
<div className="flex min-h-[400px] items-center justify-center">
|
|
<Spinner size={32} />
|
|
</div>
|
|
) : (
|
|
<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>
|
|
);
|
|
}
|