361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { z } from 'zod';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useForm } from 'react-hook-form';
|
|
import { Save, X, Loader2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form';
|
|
import { Input } from '@/components/ui/input';
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
import { Promotion, PromotionFormData } from '@/lib/types/promotion';
|
|
import { fetchClient } from '@/lib/api';
|
|
|
|
// Form schema validation with Zod (same as NewPromotionForm)
|
|
const formSchema = z.object({
|
|
code: z.string()
|
|
.min(3, 'Code must be at least 3 characters')
|
|
.max(20, 'Code cannot exceed 20 characters')
|
|
.regex(/^[A-Za-z0-9]+$/, 'Only letters and numbers are allowed'),
|
|
discountType: z.enum(['percentage', 'fixed']),
|
|
discountValue: z.coerce.number()
|
|
.positive('Discount value must be positive')
|
|
.refine(val => val <= 100, {
|
|
message: 'Percentage discount cannot exceed 100%',
|
|
path: ['discountValue'],
|
|
// Only validate this rule if discount type is percentage
|
|
params: { type: 'percentage' },
|
|
}),
|
|
minOrderAmount: z.coerce.number()
|
|
.min(0, 'Minimum order amount cannot be negative'),
|
|
maxUsage: z.coerce.number()
|
|
.nullable()
|
|
.optional(),
|
|
isActive: z.boolean().default(true),
|
|
startDate: z.string().optional(),
|
|
endDate: z.string().nullable().optional(),
|
|
description: z.string().max(200, 'Description cannot exceed 200 characters').optional(),
|
|
});
|
|
|
|
interface EditPromotionFormProps {
|
|
promotion: Promotion;
|
|
onSuccess: () => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export default function EditPromotionForm({ promotion, onSuccess, onCancel }: EditPromotionFormProps) {
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [discountType, setDiscountType] = useState<'percentage' | 'fixed'>(promotion.discountType);
|
|
|
|
// Format dates from ISO to YYYY-MM-DD for input elements
|
|
const formatDateForInput = (dateString: string | null) => {
|
|
if (!dateString) return '';
|
|
return new Date(dateString).toISOString().split('T')[0];
|
|
};
|
|
|
|
// Initialize form with promotion values
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
code: promotion.code,
|
|
discountType: promotion.discountType,
|
|
discountValue: promotion.discountValue,
|
|
minOrderAmount: promotion.minOrderAmount,
|
|
maxUsage: promotion.maxUsage,
|
|
isActive: promotion.isActive,
|
|
description: promotion.description,
|
|
startDate: formatDateForInput(promotion.startDate),
|
|
endDate: formatDateForInput(promotion.endDate),
|
|
},
|
|
});
|
|
|
|
// Keep local state in sync with form
|
|
useEffect(() => {
|
|
const subscription = form.watch((value, { name }) => {
|
|
if (name === 'discountType') {
|
|
setDiscountType(value.discountType as 'percentage' | 'fixed');
|
|
}
|
|
});
|
|
return () => subscription.unsubscribe();
|
|
}, [form, form.watch]);
|
|
|
|
// Form submission handler
|
|
async function onSubmit(data: z.infer<typeof formSchema>) {
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
await fetchClient(`/promotions/${promotion._id}`, {
|
|
method: 'PUT',
|
|
body: data,
|
|
});
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: 'Promotion updated successfully',
|
|
});
|
|
|
|
onSuccess();
|
|
} catch (error) {
|
|
console.error('Error updating promotion:', error);
|
|
// Error toast is already shown by fetchClient
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
|
<div className="grid grid-cols-1 gap-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="code"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="text-sm font-medium">Promotion Code</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="SUMMER20"
|
|
{...field}
|
|
onChange={(e) => field.onChange(e.target.value.toUpperCase())}
|
|
className="h-10"
|
|
/>
|
|
</FormControl>
|
|
<FormDescription className="text-xs">
|
|
Enter a unique code for your promotion. Only letters and numbers.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-4 gap-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="discountType"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-1">
|
|
<FormLabel className="text-sm font-medium">Discount Type</FormLabel>
|
|
<FormControl>
|
|
<RadioGroup
|
|
onValueChange={(value) => {
|
|
field.onChange(value);
|
|
setDiscountType(value as 'percentage' | 'fixed');
|
|
}}
|
|
value={field.value}
|
|
className="flex flex-col space-y-2"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="percentage" id="edit-percentage" />
|
|
<label htmlFor="edit-percentage" className="cursor-pointer">Percentage (%)</label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="fixed" id="edit-fixed" />
|
|
<label htmlFor="edit-fixed" className="cursor-pointer">Fixed Amount (£)</label>
|
|
</div>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="discountValue"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-1">
|
|
<FormLabel className="text-sm font-medium">Discount Value</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
step={discountType === 'percentage' ? '1' : '0.01'}
|
|
min={0}
|
|
max={discountType === 'percentage' ? 100 : undefined}
|
|
className="h-10"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription className="text-xs">
|
|
{discountType === 'percentage'
|
|
? 'Enter a percentage (1-100%)'
|
|
: 'Enter an amount in £'}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="minOrderAmount"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-1">
|
|
<FormLabel className="text-sm font-medium">Minimum Order Amount (£)</FormLabel>
|
|
<FormControl>
|
|
<Input type="number" step="0.01" min="0" className="h-10" {...field} />
|
|
</FormControl>
|
|
<FormDescription className="text-xs">
|
|
Minimum purchase required
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="maxUsage"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-1">
|
|
<FormLabel className="text-sm font-medium">Maximum Usage Count</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
placeholder="Unlimited"
|
|
className="h-10"
|
|
{...field}
|
|
value={field.value === null ? '' : field.value}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
field.onChange(value === '' ? null : parseInt(value, 10));
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription className="text-xs">
|
|
Leave empty for unlimited usage (currently used: {promotion.usageCount} times)
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="startDate"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="text-sm font-medium">Start Date</FormLabel>
|
|
<FormControl>
|
|
<Input type="date" className="h-10" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="endDate"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="text-sm font-medium">End Date (Optional)</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="date"
|
|
className="h-10"
|
|
{...field}
|
|
value={field.value || ''}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
field.onChange(value === '' ? null : value);
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription className="text-xs">
|
|
Leave empty for no expiration
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="isActive"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="text-base">Active Status</FormLabel>
|
|
<FormDescription>
|
|
Enable or disable this promotion
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Description (Optional)</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Enter a brief description for this promotion"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Internal notes about this promotion
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex justify-end gap-3 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onCancel}
|
|
disabled={isSubmitting}
|
|
>
|
|
<X className="h-4 w-4 mr-2" />
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Updating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="h-4 w-4 mr-2" />
|
|
Update Promotion
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
);
|
|
}
|