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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
import { toast } from '@/hooks/use-toast';
|
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({
|
const parkSchema = z.object({
|
||||||
name: z.string().min(1, 'Park name is required'),
|
name: z.string().min(1, 'Park name is required'),
|
||||||
@@ -23,15 +27,32 @@ const parkSchema = z.object({
|
|||||||
closing_date: z.string().optional(),
|
closing_date: z.string().optional(),
|
||||||
website_url: z.string().url().optional().or(z.literal('')),
|
website_url: z.string().url().optional().or(z.literal('')),
|
||||||
phone: z.string().optional(),
|
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>;
|
type ParkFormData = z.infer<typeof parkSchema>;
|
||||||
|
|
||||||
interface ParkFormProps {
|
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;
|
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;
|
isEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +83,20 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
const [bannerImageId, setBannerImageId] = useState<string>(initialData?.banner_image_id || '');
|
const [bannerImageId, setBannerImageId] = useState<string>(initialData?.banner_image_id || '');
|
||||||
const [cardImageId, setCardImageId] = useState<string>(initialData?.card_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
|
// Extract Cloudflare image ID from URL
|
||||||
const extractImageId = (url: string): string => {
|
const extractImageId = (url: string): string => {
|
||||||
const match = url.match(/\/([a-f0-9-]{36})\//);
|
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 || '',
|
closing_date: initialData?.closing_date || '',
|
||||||
website_url: initialData?.website_url || '',
|
website_url: initialData?.website_url || '',
|
||||||
phone: initialData?.phone || '',
|
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) => {
|
const handleFormSubmit = async (data: ParkFormData) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await onSubmit({
|
// Build composite submission if new entities were created
|
||||||
|
const submissionContent: any = {
|
||||||
|
park: {
|
||||||
...data,
|
...data,
|
||||||
banner_image_url: bannerImage || undefined,
|
banner_image_url: bannerImage || undefined,
|
||||||
card_image_url: cardImage || undefined,
|
card_image_url: cardImage || undefined,
|
||||||
banner_image_id: bannerImageId || undefined,
|
banner_image_id: bannerImageId || undefined,
|
||||||
card_image_id: cardImageId || 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({
|
toast({
|
||||||
@@ -251,6 +314,105 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Contact Information */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -373,6 +535,32 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -201,3 +201,75 @@ export function useCompanyHeadquarters() {
|
|||||||
|
|
||||||
return { headquarters, loading };
|
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 {
|
try {
|
||||||
if (isModerator()) {
|
if (isModerator()) {
|
||||||
// Moderators can update directly
|
// Moderators can update directly
|
||||||
const { error } = await supabase
|
const updateData: any = {
|
||||||
.from('parks')
|
|
||||||
.update({
|
|
||||||
name: parkData.name,
|
name: parkData.name,
|
||||||
slug: parkData.slug,
|
slug: parkData.slug,
|
||||||
description: parkData.description,
|
description: parkData.description || null,
|
||||||
park_type: parkData.park_type,
|
park_type: parkData.park_type,
|
||||||
status: parkData.status,
|
status: parkData.status,
|
||||||
opening_date: parkData.opening_date || null,
|
opening_date: parkData.opening_date || null,
|
||||||
@@ -194,6 +192,14 @@ export default function ParkDetail() {
|
|||||||
banner_image_id: parkData.banner_image_id || null,
|
banner_image_id: parkData.banner_image_id || null,
|
||||||
card_image_url: parkData.card_image_url || 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
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('parks')
|
||||||
|
.update({
|
||||||
|
...updateData,
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.eq('id', park.id);
|
.eq('id', park.id);
|
||||||
@@ -663,7 +669,9 @@ export default function ParkDetail() {
|
|||||||
banner_image_url: park?.banner_image_url,
|
banner_image_url: park?.banner_image_url,
|
||||||
banner_image_id: park?.banner_image_id,
|
banner_image_id: park?.banner_image_id,
|
||||||
card_image_url: park?.card_image_url,
|
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}
|
isEditing={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ export default function Parks() {
|
|||||||
banner_image_url: parkData.banner_image_url || null,
|
banner_image_url: parkData.banner_image_url || null,
|
||||||
banner_image_id: parkData.banner_image_id || null,
|
banner_image_id: parkData.banner_image_id || null,
|
||||||
card_image_url: parkData.card_image_url || 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;
|
if (error) throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user