feat: Add park operators and owners to form

This commit is contained in:
gpt-engineer-app[bot]
2025-09-30 00:14:24 +00:00
parent 0ddae7493c
commit 28feea6264
4 changed files with 292 additions and 22 deletions

View File

@@ -11,7 +11,11 @@ import { UppyPhotoUpload } from '@/components/upload/UppyPhotoUpload';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DatePicker } from '@/components/ui/date-picker';
import { toast } from '@/hooks/use-toast';
import { MapPin, Save, X } from 'lucide-react';
import { MapPin, Save, X, Plus } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Combobox } from '@/components/ui/combobox';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { useOperators, usePropertyOwners } from '@/hooks/useAutocompleteData';
const parkSchema = z.object({
name: z.string().min(1, 'Park name is required'),
@@ -23,15 +27,32 @@ const parkSchema = z.object({
closing_date: z.string().optional(),
website_url: z.string().url().optional().or(z.literal('')),
phone: z.string().optional(),
email: z.string().email().optional().or(z.literal(''))
email: z.string().email().optional().or(z.literal('')),
operator_id: z.string().uuid().optional(),
property_owner_id: z.string().uuid().optional()
});
type ParkFormData = z.infer<typeof parkSchema>;
interface ParkFormProps {
onSubmit: (data: ParkFormData & { banner_image_url?: string; card_image_url?: string; banner_image_id?: string; card_image_id?: string }) => Promise<void>;
onSubmit: (data: ParkFormData & {
banner_image_url?: string;
card_image_url?: string;
banner_image_id?: string;
card_image_id?: string;
operator_id?: string;
property_owner_id?: string;
_compositeSubmission?: any;
}) => Promise<void>;
onCancel?: () => void;
initialData?: Partial<ParkFormData & { banner_image_url?: string; card_image_url?: string; banner_image_id?: string; card_image_id?: string }>;
initialData?: Partial<ParkFormData & {
banner_image_url?: string;
card_image_url?: string;
banner_image_id?: string;
card_image_id?: string;
operator_id?: string;
property_owner_id?: string;
}>;
isEditing?: boolean;
}
@@ -61,6 +82,20 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
const [cardImage, setCardImage] = useState<string>(initialData?.card_image_url || '');
const [bannerImageId, setBannerImageId] = useState<string>(initialData?.banner_image_id || '');
const [cardImageId, setCardImageId] = useState<string>(initialData?.card_image_id || '');
// Operator state
const [selectedOperatorId, setSelectedOperatorId] = useState<string>(initialData?.operator_id || '');
const [tempNewOperator, setTempNewOperator] = useState<any>(null);
const [isOperatorModalOpen, setIsOperatorModalOpen] = useState(false);
// Property Owner state
const [selectedPropertyOwnerId, setSelectedPropertyOwnerId] = useState<string>(initialData?.property_owner_id || '');
const [tempNewPropertyOwner, setTempNewPropertyOwner] = useState<any>(null);
const [isPropertyOwnerModalOpen, setIsPropertyOwnerModalOpen] = useState(false);
// Fetch data
const { operators, loading: operatorsLoading } = useOperators();
const { propertyOwners, loading: ownersLoading } = usePropertyOwners();
// Extract Cloudflare image ID from URL
const extractImageId = (url: string): string => {
@@ -86,7 +121,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
closing_date: initialData?.closing_date || '',
website_url: initialData?.website_url || '',
phone: initialData?.phone || '',
email: initialData?.email || ''
email: initialData?.email || '',
operator_id: initialData?.operator_id || undefined,
property_owner_id: initialData?.property_owner_id || undefined
}
});
@@ -108,12 +145,38 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
const handleFormSubmit = async (data: ParkFormData) => {
setSubmitting(true);
try {
// Build composite submission if new entities were created
const submissionContent: any = {
park: {
...data,
banner_image_url: bannerImage || undefined,
card_image_url: cardImage || undefined,
banner_image_id: bannerImageId || undefined,
card_image_id: cardImageId || undefined
},
};
// Add new operator if created
if (tempNewOperator) {
submissionContent.new_operator = tempNewOperator;
submissionContent.park.operator_id = null;
}
// Add new property owner if created
if (tempNewPropertyOwner) {
submissionContent.new_property_owner = tempNewPropertyOwner;
submissionContent.park.property_owner_id = null;
}
await onSubmit({
...data,
banner_image_url: bannerImage || undefined,
card_image_url: cardImage || undefined,
banner_image_id: bannerImageId || undefined,
card_image_id: cardImageId || undefined
card_image_id: cardImageId || undefined,
operator_id: tempNewOperator ? undefined : (selectedOperatorId || undefined),
property_owner_id: tempNewPropertyOwner ? undefined : (selectedPropertyOwnerId || undefined),
_compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined
});
toast({
@@ -251,6 +314,105 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
</div>
</div>
{/* Operator & Property Owner Selection */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Operator & Property Owner</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Operator Column */}
<div className="space-y-2">
<Label>Park Operator</Label>
{tempNewOperator ? (
<div className="flex items-center gap-2 p-3 border rounded-md bg-blue-50 dark:bg-blue-950">
<Badge variant="secondary">New</Badge>
<span className="font-medium">{tempNewOperator.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setTempNewOperator(null)}
>
<X className="w-4 h-4" />
</Button>
</div>
) : (
<Combobox
options={operators}
value={watch('operator_id')}
onValueChange={(value) => {
setValue('operator_id', value);
setSelectedOperatorId(value);
}}
placeholder="Select operator"
searchPlaceholder="Search operators..."
emptyText="No operators found"
loading={operatorsLoading}
/>
)}
{!tempNewOperator && (
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => setIsOperatorModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create New Operator
</Button>
)}
</div>
{/* Property Owner Column */}
<div className="space-y-2">
<Label>Property Owner</Label>
{tempNewPropertyOwner ? (
<div className="flex items-center gap-2 p-3 border rounded-md bg-green-50 dark:bg-green-950">
<Badge variant="secondary">New</Badge>
<span className="font-medium">{tempNewPropertyOwner.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setTempNewPropertyOwner(null)}
>
<X className="w-4 h-4" />
</Button>
</div>
) : (
<Combobox
options={propertyOwners}
value={watch('property_owner_id')}
onValueChange={(value) => {
setValue('property_owner_id', value);
setSelectedPropertyOwnerId(value);
}}
placeholder="Select property owner"
searchPlaceholder="Search property owners..."
emptyText="No property owners found"
loading={ownersLoading}
/>
)}
{!tempNewPropertyOwner && (
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => setIsPropertyOwnerModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create New Property Owner
</Button>
)}
</div>
</div>
</div>
{/* Contact Information */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
@@ -373,6 +535,32 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
)}
</div>
</form>
{/* Operator Modal - Placeholder */}
<Dialog open={isOperatorModalOpen} onOpenChange={setIsOperatorModalOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Operator</DialogTitle>
<DialogDescription>
Add a new park operator company
</DialogDescription>
</DialogHeader>
<p className="text-muted-foreground">Operator form coming soon...</p>
</DialogContent>
</Dialog>
{/* Property Owner Modal - Placeholder */}
<Dialog open={isPropertyOwnerModalOpen} onOpenChange={setIsPropertyOwnerModalOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Property Owner</DialogTitle>
<DialogDescription>
Add a new park property owner company
</DialogDescription>
</DialogHeader>
<p className="text-muted-foreground">Property owner form coming soon...</p>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);