mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 11:11:14 -05:00
Batch update all date precision handling to use expanded DatePrecision, replace hardcoded day defaults, and adjust related validation, UI, and helpers. Includes wrapper migration across Phase 1-3 functions, updates to logs, displays, and formatting utilities to align frontend with new precision values ('exact', 'month', 'year', 'decade', 'century', 'approximate').
311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import * as z from 'zod';
|
|
import { entitySchemas } from '@/lib/entityValidationSchemas';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { SlugField } from '@/components/ui/slug-field';
|
|
import { Building2, Save, X } from 'lucide-react';
|
|
import { useUserRole } from '@/hooks/useUserRole';
|
|
import { HeadquartersLocationInput } from './HeadquartersLocationInput';
|
|
import { EntityMultiImageUploader } from '@/components/upload/EntityMultiImageUploader';
|
|
import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { toast } from 'sonner';
|
|
import { handleError } from '@/lib/errorHandler';
|
|
import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils';
|
|
import type { UploadedImage } from '@/types/company';
|
|
|
|
// Zod output type (after transformation)
|
|
type ManufacturerFormData = z.infer<typeof entitySchemas.manufacturer>;
|
|
|
|
interface ManufacturerFormProps {
|
|
onSubmit: (data: ManufacturerFormData) => void;
|
|
onCancel: () => void;
|
|
initialData?: Partial<ManufacturerFormData & {
|
|
id?: string;
|
|
banner_image_url?: string;
|
|
card_image_url?: string;
|
|
}>;
|
|
}
|
|
|
|
export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps): React.JSX.Element {
|
|
const { isModerator } = useUserRole();
|
|
const { user } = useAuth();
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
setValue,
|
|
watch,
|
|
formState: { errors }
|
|
} = useForm({
|
|
resolver: zodResolver(entitySchemas.manufacturer),
|
|
defaultValues: {
|
|
name: initialData?.name || '',
|
|
slug: initialData?.slug || '',
|
|
company_type: 'manufacturer' as const,
|
|
description: initialData?.description || '',
|
|
person_type: initialData?.person_type || ('company' as const),
|
|
website_url: initialData?.website_url || '',
|
|
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
|
|
founded_date: initialData?.founded_date || (initialData?.founded_year ? `${initialData.founded_year}-01-01` : undefined),
|
|
founded_date_precision: initialData?.founded_date_precision || (initialData?.founded_year ? ('year' as const) : ('exact' as const)),
|
|
headquarters_location: initialData?.headquarters_location || '',
|
|
source_url: initialData?.source_url || '',
|
|
submission_notes: initialData?.submission_notes || '',
|
|
images: initialData?.images || { uploaded: [] }
|
|
}
|
|
});
|
|
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Building2 className="w-5 h-5" />
|
|
{initialData ? 'Edit Manufacturer' : 'Create New Manufacturer'}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit(async (data) => {
|
|
if (!user) {
|
|
toast.error('You must be logged in to submit');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const formData = {
|
|
...data,
|
|
company_type: 'manufacturer' as const,
|
|
founded_year: data.founded_year ? parseInt(String(data.founded_year)) : undefined,
|
|
banner_image_id: undefined,
|
|
banner_image_url: undefined,
|
|
card_image_id: undefined,
|
|
card_image_url: undefined,
|
|
};
|
|
|
|
await onSubmit(formData);
|
|
|
|
// Only show success toast and close if not editing through moderation queue
|
|
if (!initialData?.id) {
|
|
toast.success('Manufacturer submitted for review');
|
|
onCancel();
|
|
}
|
|
} catch (error: unknown) {
|
|
handleError(error, {
|
|
action: initialData?.id ? 'Update Manufacturer' : 'Create Manufacturer',
|
|
metadata: { companyName: data.name }
|
|
});
|
|
|
|
// Re-throw so parent can handle modal closing
|
|
throw error;
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
})} className="space-y-6">
|
|
{/* Basic Information */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Name *</Label>
|
|
<Input
|
|
id="name"
|
|
{...register('name')}
|
|
placeholder="Enter manufacturer name"
|
|
/>
|
|
{errors.name && (
|
|
<p className="text-sm text-destructive">{errors.name.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<SlugField
|
|
name={watch('name')}
|
|
slug={watch('slug')}
|
|
onSlugChange={(slug) => setValue('slug', slug)}
|
|
isModerator={isModerator()}
|
|
/>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Description</Label>
|
|
<Textarea
|
|
id="description"
|
|
{...register('description')}
|
|
placeholder="Describe the manufacturer..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* Person Type */}
|
|
<div className="space-y-2">
|
|
<Label>Entity Type *</Label>
|
|
<RadioGroup
|
|
value={watch('person_type')}
|
|
onValueChange={(value) => setValue('person_type', value as 'company' | 'individual' | 'firm' | 'organization')}
|
|
className="grid grid-cols-2 md:grid-cols-4 gap-4"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="company" id="company" />
|
|
<Label htmlFor="company" className="cursor-pointer">Company</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="individual" id="individual" />
|
|
<Label htmlFor="individual" className="cursor-pointer">Individual</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="firm" id="firm" />
|
|
<Label htmlFor="firm" className="cursor-pointer">Firm</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<RadioGroupItem value="organization" id="organization" />
|
|
<Label htmlFor="organization" className="cursor-pointer">Organization</Label>
|
|
</div>
|
|
</RadioGroup>
|
|
</div>
|
|
|
|
{/* Additional Details */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<FlexibleDateInput
|
|
value={(() => {
|
|
const dateValue = watch('founded_date');
|
|
if (!dateValue) return undefined;
|
|
return parseDateOnly(dateValue);
|
|
})()}
|
|
precision={(watch('founded_date_precision') as DatePrecision) || 'year'}
|
|
onChange={(date, precision) => {
|
|
setValue('founded_date', date ? toDateWithPrecision(date, precision) : undefined, { shouldValidate: true });
|
|
setValue('founded_date_precision', precision);
|
|
}}
|
|
label="Founded Date"
|
|
placeholder="Select founded date"
|
|
disableFuture={true}
|
|
fromYear={1800}
|
|
/>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="headquarters_location">Headquarters Location</Label>
|
|
<HeadquartersLocationInput
|
|
value={watch('headquarters_location') || ''}
|
|
onChange={(value) => setValue('headquarters_location', value)}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Search OpenStreetMap for accurate location data, or manually enter location name.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Website */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="website_url">Website URL</Label>
|
|
<Input
|
|
id="website_url"
|
|
type="url"
|
|
{...register('website_url')}
|
|
placeholder="https://example.com"
|
|
/>
|
|
{errors.website_url && (
|
|
<p className="text-sm text-destructive">{errors.website_url.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Submission Context - For Reviewers */}
|
|
<div className="space-y-4 border-t pt-6">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Badge variant="secondary" className="text-xs">
|
|
For Moderator Review
|
|
</Badge>
|
|
<p className="text-xs text-muted-foreground">
|
|
Help reviewers verify your submission
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="source_url" className="flex items-center gap-2">
|
|
Source URL
|
|
<span className="text-xs text-muted-foreground font-normal">
|
|
(Optional)
|
|
</span>
|
|
</Label>
|
|
<Input
|
|
id="source_url"
|
|
type="url"
|
|
{...register('source_url')}
|
|
placeholder="https://example.com/article"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Where did you find this information? (e.g., official website, news article, press release)
|
|
</p>
|
|
{errors.source_url && (
|
|
<p className="text-sm text-destructive">{errors.source_url.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="submission_notes" className="flex items-center gap-2">
|
|
Notes for Reviewers
|
|
<span className="text-xs text-muted-foreground font-normal">
|
|
(Optional)
|
|
</span>
|
|
</Label>
|
|
<Textarea
|
|
id="submission_notes"
|
|
{...register('submission_notes')}
|
|
placeholder="Add any context to help moderators verify this information (e.g., 'Confirmed via company website', 'Founded date approximate')"
|
|
rows={3}
|
|
maxLength={1000}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{watch('submission_notes')?.length || 0}/1000 characters
|
|
</p>
|
|
{errors.submission_notes && (
|
|
<p className="text-sm text-destructive">{errors.submission_notes.message}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Images */}
|
|
<EntityMultiImageUploader
|
|
mode={initialData ? 'edit' : 'create'}
|
|
value={watch('images') || { uploaded: [] }}
|
|
onChange={(images) => setValue('images', images)}
|
|
entityType="manufacturer"
|
|
entityId={initialData?.id}
|
|
currentBannerUrl={initialData?.banner_image_url}
|
|
currentCardUrl={initialData?.card_image_url}
|
|
/>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-3 justify-end">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onCancel}
|
|
disabled={isSubmitting}
|
|
>
|
|
<X className="w-4 h-4 mr-2" />
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
loading={isSubmitting}
|
|
loadingText="Saving..."
|
|
>
|
|
<Save className="w-4 h-4 mr-2" />
|
|
{initialData?.id ? 'Update Manufacturer' : 'Create Manufacturer'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|