Reverted to commit a06d538170
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, Pencil, Trash2, Ticket, RefreshCw, Copy } from "lucide-react";
|
||||
|
||||
interface DiscountCode {
|
||||
id: string;
|
||||
code: string;
|
||||
description: string | null;
|
||||
discount_type: string;
|
||||
discount_value: number;
|
||||
min_order_amount: number;
|
||||
max_uses: number | null;
|
||||
current_uses: number;
|
||||
valid_from: string;
|
||||
valid_until: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const DiscountCodesTab = () => {
|
||||
const { toast } = useToast();
|
||||
const [codes, setCodes] = useState<DiscountCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingCode, setEditingCode] = useState<DiscountCode | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
code: "",
|
||||
description: "",
|
||||
discount_type: "percentage",
|
||||
discount_value: "",
|
||||
min_order_amount: "0",
|
||||
max_uses: "",
|
||||
valid_until: "",
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchCodes();
|
||||
}, []);
|
||||
|
||||
const fetchCodes = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from("discount_codes")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setCodes(data || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch discount codes:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load discount codes.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
code: "",
|
||||
description: "",
|
||||
discount_type: "percentage",
|
||||
discount_value: "",
|
||||
min_order_amount: "0",
|
||||
max_uses: "",
|
||||
valid_until: "",
|
||||
is_active: true,
|
||||
});
|
||||
setEditingCode(null);
|
||||
};
|
||||
|
||||
const generateCode = () => {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let code = "";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setFormData({ ...formData, code });
|
||||
};
|
||||
|
||||
const openEditDialog = (code: DiscountCode) => {
|
||||
setEditingCode(code);
|
||||
setFormData({
|
||||
code: code.code,
|
||||
description: code.description || "",
|
||||
discount_type: code.discount_type,
|
||||
discount_value: code.discount_value.toString(),
|
||||
min_order_amount: code.min_order_amount.toString(),
|
||||
max_uses: code.max_uses?.toString() || "",
|
||||
valid_until: code.valid_until ? code.valid_until.split("T")[0] : "",
|
||||
is_active: code.is_active,
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.code || !formData.discount_value) {
|
||||
toast({
|
||||
title: "Validation Error",
|
||||
description: "Code and discount value are required.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const codeData = {
|
||||
code: formData.code.toUpperCase().trim(),
|
||||
description: formData.description.trim() || null,
|
||||
discount_type: formData.discount_type,
|
||||
discount_value: parseFloat(formData.discount_value),
|
||||
min_order_amount: parseFloat(formData.min_order_amount) || 0,
|
||||
max_uses: formData.max_uses ? parseInt(formData.max_uses) : null,
|
||||
valid_until: formData.valid_until ? new Date(formData.valid_until).toISOString() : null,
|
||||
is_active: formData.is_active,
|
||||
};
|
||||
|
||||
if (editingCode) {
|
||||
const { error } = await supabase
|
||||
.from("discount_codes")
|
||||
.update(codeData)
|
||||
.eq("id", editingCode.id);
|
||||
|
||||
if (error) throw error;
|
||||
toast({ title: "Success", description: "Discount code updated." });
|
||||
} else {
|
||||
const { error } = await supabase.from("discount_codes").insert(codeData);
|
||||
if (error) throw error;
|
||||
toast({ title: "Success", description: "Discount code created." });
|
||||
}
|
||||
|
||||
setDialogOpen(false);
|
||||
resetForm();
|
||||
fetchCodes();
|
||||
} catch (error: any) {
|
||||
console.error("Failed to save discount code:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to save discount code.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this discount code?")) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from("discount_codes").delete().eq("id", id);
|
||||
if (error) throw error;
|
||||
toast({ title: "Success", description: "Discount code deleted." });
|
||||
fetchCodes();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete discount code:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to delete discount code.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyCode = (code: string) => {
|
||||
navigator.clipboard.writeText(code);
|
||||
toast({ title: "Copied!", description: `Code "${code}" copied to clipboard.` });
|
||||
};
|
||||
|
||||
const toggleActive = async (code: DiscountCode) => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("discount_codes")
|
||||
.update({ is_active: !code.is_active })
|
||||
.eq("id", code.id);
|
||||
|
||||
if (error) throw error;
|
||||
fetchCodes();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle code:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const isExpired = (validUntil: string | null) => {
|
||||
if (!validUntil) return false;
|
||||
return new Date(validUntil) < new Date();
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Ticket className="h-5 w-5" />
|
||||
Discount Codes
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="icon" onClick={fetchCodes}>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
<Dialog open={dialogOpen} onOpenChange={(open) => { setDialogOpen(open); if (!open) resetForm(); }}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Code
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingCode ? "Edit Discount Code" : "Create Discount Code"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingCode ? "Update discount code details." : "Create a new discount code for customers."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Code *</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="code"
|
||||
value={formData.code}
|
||||
onChange={(e) => setFormData({ ...formData, code: e.target.value.toUpperCase() })}
|
||||
placeholder="SAVE20"
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button type="button" variant="outline" onClick={generateCode}>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Summer sale discount"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Discount Type</Label>
|
||||
<Select
|
||||
value={formData.discount_type}
|
||||
onValueChange={(value) => setFormData({ ...formData, discount_type: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="percentage">Percentage (%)</SelectItem>
|
||||
<SelectItem value="fixed">Fixed Amount ($)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="discount_value">
|
||||
Value * ({formData.discount_type === "percentage" ? "%" : "$"})
|
||||
</Label>
|
||||
<Input
|
||||
id="discount_value"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.discount_value}
|
||||
onChange={(e) => setFormData({ ...formData, discount_value: e.target.value })}
|
||||
placeholder={formData.discount_type === "percentage" ? "20" : "10.00"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="min_order">Min. Order ($)</Label>
|
||||
<Input
|
||||
id="min_order"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.min_order_amount}
|
||||
onChange={(e) => setFormData({ ...formData, min_order_amount: e.target.value })}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max_uses">Max Uses</Label>
|
||||
<Input
|
||||
id="max_uses"
|
||||
type="number"
|
||||
value={formData.max_uses}
|
||||
onChange={(e) => setFormData({ ...formData, max_uses: e.target.value })}
|
||||
placeholder="Unlimited"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="valid_until">Expires On</Label>
|
||||
<Input
|
||||
id="valid_until"
|
||||
type="date"
|
||||
value={formData.valid_until}
|
||||
onChange={(e) => setFormData({ ...formData, valid_until: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 pt-6">
|
||||
<Switch
|
||||
id="active"
|
||||
checked={formData.is_active}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, is_active: checked })}
|
||||
/>
|
||||
<Label htmlFor="active">Active</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={saving}>
|
||||
{saving ? "Saving..." : editingCode ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
<RefreshCw className="h-5 w-5 animate-spin mr-2" />
|
||||
Loading...
|
||||
</div>
|
||||
) : codes.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<Ticket className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No discount codes yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Code</TableHead>
|
||||
<TableHead>Discount</TableHead>
|
||||
<TableHead>Uses</TableHead>
|
||||
<TableHead>Expires</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{codes.map((code) => (
|
||||
<TableRow key={code.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono font-medium">{code.code}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => copyCode(code.code)}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{code.discount_type === "percentage"
|
||||
? `${code.discount_value}%`
|
||||
: `$${Number(code.discount_value).toFixed(2)}`}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{code.current_uses}{code.max_uses ? `/${code.max_uses}` : ""}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{code.valid_until
|
||||
? new Date(code.valid_until).toLocaleDateString()
|
||||
: "Never"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isExpired(code.valid_until) ? (
|
||||
<Badge variant="destructive">Expired</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant={code.is_active ? "default" : "secondary"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleActive(code)}
|
||||
>
|
||||
{code.is_active ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => openEditDialog(code)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(code.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscountCodesTab;
|
||||
Reference in New Issue
Block a user