From 61c7a551c296ad22723f07411d052047d67f91a7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:04:36 +0000 Subject: [PATCH] Approve database migration --- src/pages/DesignerDetail.tsx | 41 +++++++++- src/pages/ManufacturerDetail.tsx | 55 ++++++++++++- src/pages/OperatorDetail.tsx | 77 ++++++++++++++++++- src/pages/ParkDetail.tsx | 25 +++++- src/pages/PropertyOwnerDetail.tsx | 77 ++++++++++++++++++- src/pages/RideDetail.tsx | 25 +++++- ...6_c801888e-b914-4c63-a1d0-75b3afba48e2.sql | 4 + 7 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 supabase/migrations/20251010130246_c801888e-b914-4c63-a1d0-75b3afba48e2.sql diff --git a/src/pages/DesignerDetail.tsx b/src/pages/DesignerDetail.tsx index c3834299..47b67487 100644 --- a/src/pages/DesignerDetail.tsx +++ b/src/pages/DesignerDetail.tsx @@ -24,6 +24,9 @@ export default function DesignerDetail() { const [designer, setDesigner] = useState(null); const [loading, setLoading] = useState(true); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [totalRides, setTotalRides] = useState(0); + const [totalPhotos, setTotalPhotos] = useState(0); + const [statsLoading, setStatsLoading] = useState(true); const { user } = useAuth(); const { isModerator } = useUserRole(); @@ -44,6 +47,9 @@ export default function DesignerDetail() { if (error) throw error; setDesigner(data); + if (data) { + fetchStatistics(data.id); + } } catch (error) { console.error('Error fetching designer:', error); } finally { @@ -51,6 +57,33 @@ export default function DesignerDetail() { } }; + const fetchStatistics = async (designerId: string) => { + try { + // Count rides + const { count: ridesCount, error: ridesError } = await supabase + .from('rides') + .select('id', { count: 'exact', head: true }) + .eq('designer_id', designerId); + + if (ridesError) throw ridesError; + setTotalRides(ridesCount || 0); + + // Count photos + const { count: photosCount, error: photosError } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'designer') + .eq('entity_id', designerId); + + if (photosError) throw photosError; + setTotalPhotos(photosCount || 0); + } catch (error) { + console.error('Error fetching statistics:', error); + } finally { + setStatsLoading(false); + } + }; + const handleEditSubmit = async (data: any) => { try { await submitCompanyUpdate( @@ -229,8 +262,12 @@ export default function DesignerDetail() { Overview - Rides - Photos + + Rides {!statsLoading && totalRides > 0 && `(${totalRides})`} + + + Photos {!statsLoading && totalPhotos > 0 && `(${totalPhotos})`} + History diff --git a/src/pages/ManufacturerDetail.tsx b/src/pages/ManufacturerDetail.tsx index 9de192d2..d4f4f7f5 100644 --- a/src/pages/ManufacturerDetail.tsx +++ b/src/pages/ManufacturerDetail.tsx @@ -24,6 +24,10 @@ export default function ManufacturerDetail() { const [manufacturer, setManufacturer] = useState(null); const [loading, setLoading] = useState(true); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [totalRides, setTotalRides] = useState(0); + const [totalModels, setTotalModels] = useState(0); + const [totalPhotos, setTotalPhotos] = useState(0); + const [statsLoading, setStatsLoading] = useState(true); const { user } = useAuth(); const { isModerator } = useUserRole(); @@ -44,6 +48,9 @@ export default function ManufacturerDetail() { if (error) throw error; setManufacturer(data); + if (data) { + fetchStatistics(data.id); + } } catch (error) { console.error('Error fetching manufacturer:', error); } finally { @@ -51,6 +58,42 @@ export default function ManufacturerDetail() { } }; + const fetchStatistics = async (manufacturerId: string) => { + try { + // Count rides + const { count: ridesCount, error: ridesError } = await supabase + .from('rides') + .select('id', { count: 'exact', head: true }) + .eq('manufacturer_id', manufacturerId); + + if (ridesError) throw ridesError; + setTotalRides(ridesCount || 0); + + // Count models + const { count: modelsCount, error: modelsError } = await supabase + .from('ride_models') + .select('id', { count: 'exact', head: true }) + .eq('manufacturer_id', manufacturerId); + + if (modelsError) throw modelsError; + setTotalModels(modelsCount || 0); + + // Count photos + const { count: photosCount, error: photosError } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'manufacturer') + .eq('entity_id', manufacturerId); + + if (photosError) throw photosError; + setTotalPhotos(photosCount || 0); + } catch (error) { + console.error('Error fetching statistics:', error); + } finally { + setStatsLoading(false); + } + }; + const handleEditSubmit = async (data: any) => { try { await submitCompanyUpdate( @@ -231,9 +274,15 @@ export default function ManufacturerDetail() { Overview - Rides - Models - Photos + + Rides {!statsLoading && totalRides > 0 && `(${totalRides})`} + + + Models {!statsLoading && totalModels > 0 && `(${totalModels})`} + + + Photos {!statsLoading && totalPhotos > 0 && `(${totalPhotos})`} + History diff --git a/src/pages/OperatorDetail.tsx b/src/pages/OperatorDetail.tsx index 5daab76f..1a5b7116 100644 --- a/src/pages/OperatorDetail.tsx +++ b/src/pages/OperatorDetail.tsx @@ -30,6 +30,7 @@ export default function OperatorDetail() { const [totalParks, setTotalParks] = useState(0); const [operatingRides, setOperatingRides] = useState(0); const [statsLoading, setStatsLoading] = useState(true); + const [totalPhotos, setTotalPhotos] = useState(0); const { user } = useAuth(); const { isModerator } = useUserRole(); @@ -39,6 +40,57 @@ export default function OperatorDetail() { } }, [slug]); + // Real-time subscription for parks, rides, and photos changes + useEffect(() => { + if (!operator?.id) return; + + const channel = supabase + .channel('operator-stats-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'parks', + filter: `operator_id=eq.${operator.id}` + }, + (payload) => { + console.log('Park change detected for operator:', payload); + fetchStatistics(operator.id); + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'rides' + }, + (payload) => { + console.log('Ride status change detected:', payload); + fetchStatistics(operator.id); + } + ) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'photos', + filter: `entity_type=eq.operator,entity_id=eq.${operator.id}` + }, + (payload) => { + console.log('Photo change detected for operator:', payload); + fetchPhotoCount(operator.id); + } + ) + .subscribe(); + + return () => { + supabase.removeChannel(channel); + }; + }, [operator?.id]); + const fetchOperatorData = async () => { try { const { data, error } = await supabase @@ -55,6 +107,7 @@ export default function OperatorDetail() { if (data) { fetchParks(data.id); fetchStatistics(data.id); + fetchPhotoCount(data.id); } } catch (error) { console.error('Error fetching operator:', error); @@ -111,6 +164,22 @@ export default function OperatorDetail() { } }; + const fetchPhotoCount = async (operatorId: string) => { + try { + const { count, error } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'operator') + .eq('entity_id', operatorId); + + if (error) throw error; + setTotalPhotos(count || 0); + } catch (error) { + console.error('Error fetching photo count:', error); + setTotalPhotos(0); + } + }; + const handleEditSubmit = async (data: any) => { try { await submitCompanyUpdate( @@ -314,8 +383,12 @@ export default function OperatorDetail() { Overview - Parks - Photos + + Parks {!statsLoading && totalParks > 0 && `(${totalParks})`} + + + Photos {!statsLoading && totalPhotos > 0 && `(${totalPhotos})`} + History diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index c9b66bc8..f935c61f 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -36,6 +36,8 @@ export default function ParkDetail() { const [loading, setLoading] = useState(true); const [isAddRideModalOpen, setIsAddRideModalOpen] = useState(false); const [isEditParkModalOpen, setIsEditParkModalOpen] = useState(false); + const [photoCount, setPhotoCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(true); const { isModerator } = useUserRole(); useEffect(() => { if (slug) { @@ -55,6 +57,7 @@ export default function ParkDetail() { `).eq('slug', slug).maybeSingle(); if (parkData) { setPark(parkData); + fetchPhotoCount(parkData.id); // Fetch park rides const { @@ -68,6 +71,24 @@ export default function ParkDetail() { setLoading(false); } }; + + const fetchPhotoCount = async (parkId: string) => { + try { + const { count, error } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'park') + .eq('entity_id', parkId); + + if (error) throw error; + setPhotoCount(count || 0); + } catch (error) { + console.error('Error fetching photo count:', error); + setPhotoCount(0); + } finally { + setStatsLoading(false); + } + }; const getStatusColor = (status: string) => { switch (status) { case 'operating': @@ -361,7 +382,9 @@ export default function ParkDetail() { Overview Rides ({rides.length}) Reviews - Photos + + Photos {!statsLoading && photoCount > 0 && `(${photoCount})`} + History diff --git a/src/pages/PropertyOwnerDetail.tsx b/src/pages/PropertyOwnerDetail.tsx index da3f6704..bb968e10 100644 --- a/src/pages/PropertyOwnerDetail.tsx +++ b/src/pages/PropertyOwnerDetail.tsx @@ -30,6 +30,7 @@ export default function PropertyOwnerDetail() { const [totalParks, setTotalParks] = useState(0); const [operatingRides, setOperatingRides] = useState(0); const [statsLoading, setStatsLoading] = useState(true); + const [totalPhotos, setTotalPhotos] = useState(0); const { user } = useAuth(); const { isModerator } = useUserRole(); @@ -39,6 +40,57 @@ export default function PropertyOwnerDetail() { } }, [slug]); + // Real-time subscription for parks and photos changes + useEffect(() => { + if (!owner?.id) return; + + const channel = supabase + .channel('owner-stats-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'parks', + filter: `property_owner_id=eq.${owner.id}` + }, + (payload) => { + console.log('Park change detected for owner:', payload); + fetchStatistics(owner.id); + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'rides' + }, + (payload) => { + console.log('Ride status change detected:', payload); + fetchStatistics(owner.id); + } + ) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'photos', + filter: `entity_type=eq.property_owner,entity_id=eq.${owner.id}` + }, + (payload) => { + console.log('Photo change detected for owner:', payload); + fetchPhotoCount(owner.id); + } + ) + .subscribe(); + + return () => { + supabase.removeChannel(channel); + }; + }, [owner?.id]); + const fetchOwnerData = async () => { try { const { data, error } = await supabase @@ -55,6 +107,7 @@ export default function PropertyOwnerDetail() { if (data) { fetchParks(data.id); fetchStatistics(data.id); + fetchPhotoCount(data.id); } } catch (error) { console.error('Error fetching property owner:', error); @@ -111,6 +164,22 @@ export default function PropertyOwnerDetail() { } }; + const fetchPhotoCount = async (ownerId: string) => { + try { + const { count, error } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'property_owner') + .eq('entity_id', ownerId); + + if (error) throw error; + setTotalPhotos(count || 0); + } catch (error) { + console.error('Error fetching photo count:', error); + setTotalPhotos(0); + } + }; + const handleEditSubmit = async (data: any) => { try { await submitCompanyUpdate( @@ -314,8 +383,12 @@ export default function PropertyOwnerDetail() { Overview - Parks - Photos + + Parks {!statsLoading && totalParks > 0 && `(${totalParks})`} + + + Photos {!statsLoading && totalPhotos > 0 && `(${totalPhotos})`} + History diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index b3326961..257d0c91 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -53,6 +53,8 @@ export default function RideDetail() { const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState("overview"); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [photoCount, setPhotoCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(true); const { user } = useAuth(); const { isModerator } = useUserRole(); @@ -88,6 +90,7 @@ export default function RideDetail() { if (rideData) { // Store park_id for easier access (rideData as any).currentParkId = parkData.id; + fetchPhotoCount(rideData.id); } setRide(rideData); @@ -99,6 +102,24 @@ export default function RideDetail() { } }; + const fetchPhotoCount = async (rideId: string) => { + try { + const { count, error } = await supabase + .from('photos') + .select('id', { count: 'exact', head: true }) + .eq('entity_type', 'ride') + .eq('entity_id', rideId); + + if (error) throw error; + setPhotoCount(count || 0); + } catch (error) { + console.error('Error fetching photo count:', error); + setPhotoCount(0); + } finally { + setStatsLoading(false); + } + }; + const getStatusColor = (status: string) => { switch (status) { case 'operating': return 'bg-green-500/20 text-green-400 border-green-500/30'; @@ -387,7 +408,9 @@ export default function RideDetail() { Overview Specifications Reviews - Photos + + Photos {!statsLoading && photoCount > 0 && `(${photoCount})`} + History diff --git a/supabase/migrations/20251010130246_c801888e-b914-4c63-a1d0-75b3afba48e2.sql b/supabase/migrations/20251010130246_c801888e-b914-4c63-a1d0-75b3afba48e2.sql new file mode 100644 index 00000000..5e8740c4 --- /dev/null +++ b/supabase/migrations/20251010130246_c801888e-b914-4c63-a1d0-75b3afba48e2.sql @@ -0,0 +1,4 @@ +-- Enable replica identity for real-time change tracking +ALTER TABLE parks REPLICA IDENTITY FULL; +ALTER TABLE rides REPLICA IDENTITY FULL; +ALTER TABLE photos REPLICA IDENTITY FULL; \ No newline at end of file