mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 07:11:12 -05:00
feat: Add park operators and owners to form
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -62,6 +83,20 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
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 => {
|
||||
const match = url.match(/\/([a-f0-9-]{36})\//);
|
||||
@@ -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 {
|
||||
await onSubmit({
|
||||
// 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,
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -201,3 +201,75 @@ export function useCompanyHeadquarters() {
|
||||
|
||||
return { headquarters, loading };
|
||||
}
|
||||
|
||||
export function useOperators() {
|
||||
const [operators, setOperators] = useState<ComboboxOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchOperators() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('companies')
|
||||
.select('id, name')
|
||||
.eq('company_type', 'operator')
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setOperators(
|
||||
(data || []).map(company => ({
|
||||
label: company.name,
|
||||
value: company.id
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching operators:', error);
|
||||
setOperators([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchOperators();
|
||||
}, []);
|
||||
|
||||
return { operators, loading };
|
||||
}
|
||||
|
||||
export function usePropertyOwners() {
|
||||
const [propertyOwners, setPropertyOwners] = useState<ComboboxOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPropertyOwners() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('companies')
|
||||
.select('id, name')
|
||||
.eq('company_type', 'property_owner')
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
setPropertyOwners(
|
||||
(data || []).map(company => ({
|
||||
label: company.name,
|
||||
value: company.id
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching property owners:', error);
|
||||
setPropertyOwners([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchPropertyOwners();
|
||||
}, []);
|
||||
|
||||
return { propertyOwners, loading };
|
||||
}
|
||||
@@ -177,12 +177,10 @@ export default function ParkDetail() {
|
||||
try {
|
||||
if (isModerator()) {
|
||||
// Moderators can update directly
|
||||
const { error } = await supabase
|
||||
.from('parks')
|
||||
.update({
|
||||
const updateData: any = {
|
||||
name: parkData.name,
|
||||
slug: parkData.slug,
|
||||
description: parkData.description,
|
||||
description: parkData.description || null,
|
||||
park_type: parkData.park_type,
|
||||
status: parkData.status,
|
||||
opening_date: parkData.opening_date || null,
|
||||
@@ -194,6 +192,14 @@ export default function ParkDetail() {
|
||||
banner_image_id: parkData.banner_image_id || null,
|
||||
card_image_url: parkData.card_image_url || null,
|
||||
card_image_id: parkData.card_image_id || null,
|
||||
operator_id: parkData.operator_id || null,
|
||||
property_owner_id: parkData.property_owner_id || null
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from('parks')
|
||||
.update({
|
||||
...updateData,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', park.id);
|
||||
@@ -663,7 +669,9 @@ export default function ParkDetail() {
|
||||
banner_image_url: park?.banner_image_url,
|
||||
banner_image_id: park?.banner_image_id,
|
||||
card_image_url: park?.card_image_url,
|
||||
card_image_id: park?.card_image_id
|
||||
card_image_id: park?.card_image_id,
|
||||
operator_id: park?.operator?.id,
|
||||
property_owner_id: park?.property_owner?.id
|
||||
}}
|
||||
isEditing={true}
|
||||
/>
|
||||
|
||||
@@ -268,7 +268,9 @@ export default function Parks() {
|
||||
banner_image_url: parkData.banner_image_url || null,
|
||||
banner_image_id: parkData.banner_image_id || null,
|
||||
card_image_url: parkData.card_image_url || null,
|
||||
card_image_id: parkData.card_image_id || null
|
||||
card_image_id: parkData.card_image_id || null,
|
||||
operator_id: parkData.operator_id || null,
|
||||
property_owner_id: parkData.property_owner_id || null
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
Reference in New Issue
Block a user