Files
ember-market-frontend/components/dashboard/promotions/NewPromotionForm.tsx
NotII 1b51f29c24 Add flexible date pickers and export options to stock dashboard
Introduces a reusable date picker component with support for single date, date range, and month selection. Updates the stock management page to allow exporting reports by daily, weekly, monthly, or custom date ranges using the new pickers. Refactors promotion form to use the new date picker for start and end dates. Adds more business quotes to the quotes config.
2025-07-30 00:38:25 +02:00

356 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 { Plus, 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 { PromotionFormData } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
import { DatePicker } from '@/components/ui/date-picker';
// Form schema validation with Zod
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 NewPromotionFormProps {
onSuccess: () => void;
onCancel: () => void;
}
export default function NewPromotionForm({ onSuccess, onCancel }: NewPromotionFormProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const [discountType, setDiscountType] = useState<'percentage' | 'fixed'>('percentage');
// Initialize form with default values
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
code: '',
discountType: 'percentage',
discountValue: 10,
minOrderAmount: 0,
maxUsage: null,
isActive: true,
description: '',
startDate: new Date().toISOString().split('T')[0], // Today
endDate: null,
},
});
// 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', {
method: 'POST',
body: data,
});
toast({
title: 'Success',
description: 'Promotion created successfully',
});
onSuccess();
} catch (error) {
console.error('Error creating 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="percentage" />
<label htmlFor="percentage" className="cursor-pointer">Percentage (%)</label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="fixed" id="fixed" />
<label htmlFor="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
</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>
<DatePicker
date={field.value ? new Date(field.value) : undefined}
onDateChange={(date) => field.onChange(date ? date.toISOString().split('T')[0] : '')}
placeholder="Select start date"
className="h-10"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="endDate"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium">End Date (Optional)</FormLabel>
<FormControl>
<DatePicker
date={field.value ? new Date(field.value) : undefined}
onDateChange={(date) => field.onChange(date ? date.toISOString().split('T')[0] : null)}
placeholder="Select end date (optional)"
className="h-10"
/>
</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" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Create Promotion
</>
)}
</Button>
</div>
</form>
</Form>
);
}