mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Refactor: Implement Phase 3A technical data migration
This commit is contained in:
68
src/components/rides/CoasterStatistics.tsx
Normal file
68
src/components/rides/CoasterStatistics.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { RideCoasterStat } from "@/types/database";
|
||||||
|
import { TrendingUp } from "lucide-react";
|
||||||
|
|
||||||
|
interface CoasterStatisticsProps {
|
||||||
|
statistics?: RideCoasterStat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoasterStatistics = ({ statistics }: CoasterStatisticsProps) => {
|
||||||
|
if (!statistics || statistics.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group stats by category
|
||||||
|
const groupedStats = statistics.reduce((acc, stat) => {
|
||||||
|
const category = stat.category || 'General';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = [];
|
||||||
|
}
|
||||||
|
acc[category].push(stat);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, RideCoasterStat[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5" />
|
||||||
|
Coaster Statistics
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedStats).map(([category, stats]) => (
|
||||||
|
<div key={category}>
|
||||||
|
<h3 className="text-sm font-semibold mb-3 text-muted-foreground uppercase tracking-wide">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{stats
|
||||||
|
.sort((a, b) => a.display_order - b.display_order)
|
||||||
|
.map((stat) => (
|
||||||
|
<div
|
||||||
|
key={stat.id}
|
||||||
|
className="flex justify-between items-center p-3 rounded-lg bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">{stat.stat_name}</span>
|
||||||
|
{stat.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{stat.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold">
|
||||||
|
{stat.stat_value.toLocaleString()}
|
||||||
|
{stat.unit && ` ${stat.unit}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
|
import { RideNameHistory } from '@/types/database';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
interface FormerName {
|
interface FormerName {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -9,14 +11,41 @@ interface FormerName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FormerNamesProps {
|
interface FormerNamesProps {
|
||||||
formerNames: FormerName[];
|
formerNames?: FormerName[];
|
||||||
|
nameHistory?: RideNameHistory[]; // New relational data
|
||||||
currentName: string;
|
currentName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormerNames({ formerNames, currentName }: FormerNamesProps) {
|
export function FormerNames({ formerNames, nameHistory, currentName }: FormerNamesProps) {
|
||||||
if (!formerNames || formerNames.length === 0) {
|
// Use new relational data if available, fallback to legacy JSON
|
||||||
|
const names = nameHistory || formerNames;
|
||||||
|
|
||||||
|
if (!names || names.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize data structure
|
||||||
|
const normalizedNames = names.map((item) => {
|
||||||
|
if ('former_name' in item) {
|
||||||
|
// New relational format (RideNameHistory)
|
||||||
|
return {
|
||||||
|
name: item.former_name,
|
||||||
|
date_changed: item.date_changed,
|
||||||
|
reason: item.reason,
|
||||||
|
from_year: item.from_year,
|
||||||
|
to_year: item.to_year,
|
||||||
|
order: item.order_index,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Legacy JSON format (FormerName)
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
from_year: item.from_year,
|
||||||
|
to_year: item.to_year,
|
||||||
|
order: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -37,21 +66,29 @@ export function FormerNames({ formerNames, currentName }: FormerNamesProps) {
|
|||||||
<Badge variant="default">Current</Badge>
|
<Badge variant="default">Current</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formerNames.map((former, index) => (
|
{normalizedNames.map((former, index) => (
|
||||||
<div key={index} className="flex items-start gap-3">
|
<div key={index} className="flex items-start gap-3">
|
||||||
<div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-muted-foreground" />
|
<div className="flex-shrink-0 w-2 h-2 mt-2 rounded-full bg-muted-foreground" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-medium">{former.name}</div>
|
<div className="font-medium">{former.name}</div>
|
||||||
{(former.from_year || former.to_year) && (
|
<div className="text-sm text-muted-foreground space-y-1">
|
||||||
<div className="text-sm text-muted-foreground">
|
{(former.from_year || former.to_year) && (
|
||||||
{former.from_year && former.to_year
|
<div>
|
||||||
? `${former.from_year} - ${former.to_year}`
|
{former.from_year && former.to_year
|
||||||
: former.from_year
|
? `${former.from_year} - ${former.to_year}`
|
||||||
? `Since ${former.from_year}`
|
: former.from_year
|
||||||
: `Until ${former.to_year}`
|
? `Since ${former.from_year}`
|
||||||
}
|
: `Until ${former.to_year}`
|
||||||
</div>
|
}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{former.date_changed && (
|
||||||
|
<div>Changed: {format(new Date(former.date_changed), 'MMM d, yyyy')}</div>
|
||||||
|
)}
|
||||||
|
{former.reason && (
|
||||||
|
<div className="italic">{former.reason}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline">Former</Badge>
|
<Badge variant="outline">Former</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
61
src/components/rides/TechnicalSpecifications.tsx
Normal file
61
src/components/rides/TechnicalSpecifications.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { RideTechnicalSpec } from "@/types/database";
|
||||||
|
import { Wrench } from "lucide-react";
|
||||||
|
|
||||||
|
interface TechnicalSpecificationsProps {
|
||||||
|
specifications?: RideTechnicalSpec[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TechnicalSpecifications = ({ specifications }: TechnicalSpecificationsProps) => {
|
||||||
|
if (!specifications || specifications.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group specs by category
|
||||||
|
const groupedSpecs = specifications.reduce((acc, spec) => {
|
||||||
|
const category = spec.category || 'General';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = [];
|
||||||
|
}
|
||||||
|
acc[category].push(spec);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, RideTechnicalSpec[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Wrench className="h-5 w-5" />
|
||||||
|
Technical Specifications
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{Object.entries(groupedSpecs).map(([category, specs]) => (
|
||||||
|
<div key={category}>
|
||||||
|
<h3 className="text-sm font-semibold mb-3 text-muted-foreground uppercase tracking-wide">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{specs
|
||||||
|
.sort((a, b) => a.display_order - b.display_order)
|
||||||
|
.map((spec) => (
|
||||||
|
<div
|
||||||
|
key={spec.id}
|
||||||
|
className="flex justify-between items-center p-3 rounded-lg bg-muted/50"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-medium">{spec.spec_name}</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{spec.spec_value}
|
||||||
|
{spec.unit && ` ${spec.unit}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1103,6 +1103,50 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ride_coaster_statistics: {
|
||||||
|
Row: {
|
||||||
|
category: string | null
|
||||||
|
created_at: string
|
||||||
|
description: string | null
|
||||||
|
display_order: number | null
|
||||||
|
id: string
|
||||||
|
ride_id: string
|
||||||
|
stat_name: string
|
||||||
|
stat_value: number
|
||||||
|
unit: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
description?: string | null
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_id: string
|
||||||
|
stat_name: string
|
||||||
|
stat_value: number
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
description?: string | null
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_id?: string
|
||||||
|
stat_name?: string
|
||||||
|
stat_value?: number
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "ride_coaster_statistics_ride_id_fkey"
|
||||||
|
columns: ["ride_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "rides"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
ride_coaster_stats: {
|
ride_coaster_stats: {
|
||||||
Row: {
|
Row: {
|
||||||
category: string | null
|
category: string | null
|
||||||
@@ -1238,6 +1282,50 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ride_model_technical_specifications: {
|
||||||
|
Row: {
|
||||||
|
category: string | null
|
||||||
|
created_at: string
|
||||||
|
display_order: number | null
|
||||||
|
id: string
|
||||||
|
ride_model_id: string
|
||||||
|
spec_name: string
|
||||||
|
spec_type: string
|
||||||
|
spec_value: string
|
||||||
|
unit: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_model_id: string
|
||||||
|
spec_name: string
|
||||||
|
spec_type: string
|
||||||
|
spec_value: string
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_model_id?: string
|
||||||
|
spec_name?: string
|
||||||
|
spec_type?: string
|
||||||
|
spec_value?: string
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "ride_model_technical_specifications_ride_model_id_fkey"
|
||||||
|
columns: ["ride_model_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "ride_models"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
ride_models: {
|
ride_models: {
|
||||||
Row: {
|
Row: {
|
||||||
banner_image_id: string | null
|
banner_image_id: string | null
|
||||||
@@ -1297,6 +1385,50 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ride_name_history: {
|
||||||
|
Row: {
|
||||||
|
created_at: string
|
||||||
|
date_changed: string | null
|
||||||
|
former_name: string
|
||||||
|
from_year: number | null
|
||||||
|
id: string
|
||||||
|
order_index: number | null
|
||||||
|
reason: string | null
|
||||||
|
ride_id: string
|
||||||
|
to_year: number | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string
|
||||||
|
date_changed?: string | null
|
||||||
|
former_name: string
|
||||||
|
from_year?: number | null
|
||||||
|
id?: string
|
||||||
|
order_index?: number | null
|
||||||
|
reason?: string | null
|
||||||
|
ride_id: string
|
||||||
|
to_year?: number | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string
|
||||||
|
date_changed?: string | null
|
||||||
|
former_name?: string
|
||||||
|
from_year?: number | null
|
||||||
|
id?: string
|
||||||
|
order_index?: number | null
|
||||||
|
reason?: string | null
|
||||||
|
ride_id?: string
|
||||||
|
to_year?: number | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "ride_name_history_ride_id_fkey"
|
||||||
|
columns: ["ride_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "rides"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
ride_submissions: {
|
ride_submissions: {
|
||||||
Row: {
|
Row: {
|
||||||
age_requirement: number | null
|
age_requirement: number | null
|
||||||
@@ -1416,6 +1548,50 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
ride_technical_specifications: {
|
||||||
|
Row: {
|
||||||
|
category: string | null
|
||||||
|
created_at: string
|
||||||
|
display_order: number | null
|
||||||
|
id: string
|
||||||
|
ride_id: string
|
||||||
|
spec_name: string
|
||||||
|
spec_type: string
|
||||||
|
spec_value: string
|
||||||
|
unit: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_id: string
|
||||||
|
spec_name: string
|
||||||
|
spec_type: string
|
||||||
|
spec_value: string
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
category?: string | null
|
||||||
|
created_at?: string
|
||||||
|
display_order?: number | null
|
||||||
|
id?: string
|
||||||
|
ride_id?: string
|
||||||
|
spec_name?: string
|
||||||
|
spec_type?: string
|
||||||
|
spec_value?: string
|
||||||
|
unit?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "ride_technical_specifications_ride_id_fkey"
|
||||||
|
columns: ["ride_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "rides"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
ride_technical_specs: {
|
ride_technical_specs: {
|
||||||
Row: {
|
Row: {
|
||||||
category: string | null
|
category: string | null
|
||||||
@@ -2006,6 +2182,10 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
}
|
}
|
||||||
|
migrate_ride_technical_data: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
update_company_ratings: {
|
update_company_ratings: {
|
||||||
Args: { target_company_id: string }
|
Args: { target_company_id: string }
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
|
|||||||
@@ -52,6 +52,54 @@ export interface Park {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RideTechnicalSpec {
|
||||||
|
id: string;
|
||||||
|
ride_id: string;
|
||||||
|
spec_name: string;
|
||||||
|
spec_value: string;
|
||||||
|
spec_type: 'string' | 'number' | 'boolean' | 'date';
|
||||||
|
category?: string;
|
||||||
|
unit?: string;
|
||||||
|
display_order: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RideCoasterStat {
|
||||||
|
id: string;
|
||||||
|
ride_id: string;
|
||||||
|
stat_name: string;
|
||||||
|
stat_value: number;
|
||||||
|
unit?: string;
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
display_order: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RideNameHistory {
|
||||||
|
id: string;
|
||||||
|
ride_id: string;
|
||||||
|
former_name: string;
|
||||||
|
date_changed?: string;
|
||||||
|
reason?: string;
|
||||||
|
from_year?: number;
|
||||||
|
to_year?: number;
|
||||||
|
order_index: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RideModelTechnicalSpec {
|
||||||
|
id: string;
|
||||||
|
ride_model_id: string;
|
||||||
|
spec_name: string;
|
||||||
|
spec_value: string;
|
||||||
|
spec_type: 'string' | 'number' | 'boolean' | 'date';
|
||||||
|
category?: string;
|
||||||
|
unit?: string;
|
||||||
|
display_order: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RideModel {
|
export interface RideModel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -60,7 +108,8 @@ export interface RideModel {
|
|||||||
category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation';
|
category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation';
|
||||||
ride_type: string;
|
ride_type: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
technical_specs?: any;
|
technical_specs?: any; // Legacy JSON field
|
||||||
|
technical_specifications?: RideModelTechnicalSpec[]; // New relational data
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ride {
|
export interface Ride {
|
||||||
@@ -85,8 +134,13 @@ export interface Ride {
|
|||||||
max_height_meters?: number;
|
max_height_meters?: number;
|
||||||
length_meters?: number;
|
length_meters?: number;
|
||||||
inversions?: number;
|
inversions?: number;
|
||||||
coaster_stats?: any;
|
coaster_stats?: any; // Legacy JSON field
|
||||||
technical_specs?: any;
|
technical_specs?: any; // Legacy JSON field
|
||||||
|
former_names?: any; // Legacy JSON field
|
||||||
|
// New relational data
|
||||||
|
technical_specifications?: RideTechnicalSpec[];
|
||||||
|
coaster_statistics?: RideCoasterStat[];
|
||||||
|
name_history?: RideNameHistory[];
|
||||||
average_rating: number;
|
average_rating: number;
|
||||||
review_count: number;
|
review_count: number;
|
||||||
image_url?: string;
|
image_url?: string;
|
||||||
@@ -100,7 +154,6 @@ export interface Ride {
|
|||||||
intensity_level?: string;
|
intensity_level?: string;
|
||||||
drop_height_meters?: number;
|
drop_height_meters?: number;
|
||||||
max_g_force?: number;
|
max_g_force?: number;
|
||||||
former_names?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
-- Phase 3A: Create Production Relational Tables and Data Migration
|
||||||
|
|
||||||
|
-- 1. Ride Technical Specifications
|
||||||
|
CREATE TABLE ride_technical_specifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ride_id UUID NOT NULL REFERENCES rides(id) ON DELETE CASCADE,
|
||||||
|
spec_name TEXT NOT NULL,
|
||||||
|
spec_value TEXT NOT NULL,
|
||||||
|
spec_type TEXT NOT NULL CHECK (spec_type IN ('string', 'number', 'boolean', 'date')),
|
||||||
|
category TEXT,
|
||||||
|
unit TEXT,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT ride_technical_specifications_unique_spec UNIQUE(ride_id, spec_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ride_technical_specifications_ride_id ON ride_technical_specifications(ride_id);
|
||||||
|
CREATE INDEX idx_ride_technical_specifications_category ON ride_technical_specifications(category);
|
||||||
|
|
||||||
|
-- 2. Ride Coaster Statistics
|
||||||
|
CREATE TABLE ride_coaster_statistics (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ride_id UUID NOT NULL REFERENCES rides(id) ON DELETE CASCADE,
|
||||||
|
stat_name TEXT NOT NULL,
|
||||||
|
stat_value NUMERIC NOT NULL,
|
||||||
|
unit TEXT,
|
||||||
|
category TEXT,
|
||||||
|
description TEXT,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT ride_coaster_statistics_unique_stat UNIQUE(ride_id, stat_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ride_coaster_statistics_ride_id ON ride_coaster_statistics(ride_id);
|
||||||
|
CREATE INDEX idx_ride_coaster_statistics_category ON ride_coaster_statistics(category);
|
||||||
|
|
||||||
|
-- 3. Ride Name History
|
||||||
|
CREATE TABLE ride_name_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ride_id UUID NOT NULL REFERENCES rides(id) ON DELETE CASCADE,
|
||||||
|
former_name TEXT NOT NULL,
|
||||||
|
date_changed DATE,
|
||||||
|
reason TEXT,
|
||||||
|
from_year INTEGER,
|
||||||
|
to_year INTEGER,
|
||||||
|
order_index INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ride_name_history_ride_id ON ride_name_history(ride_id);
|
||||||
|
CREATE INDEX idx_ride_name_history_date_changed ON ride_name_history(date_changed);
|
||||||
|
|
||||||
|
-- 4. Ride Model Technical Specifications
|
||||||
|
CREATE TABLE ride_model_technical_specifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ride_model_id UUID NOT NULL REFERENCES ride_models(id) ON DELETE CASCADE,
|
||||||
|
spec_name TEXT NOT NULL,
|
||||||
|
spec_value TEXT NOT NULL,
|
||||||
|
spec_type TEXT NOT NULL CHECK (spec_type IN ('string', 'number', 'boolean', 'date')),
|
||||||
|
category TEXT,
|
||||||
|
unit TEXT,
|
||||||
|
display_order INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT ride_model_technical_specifications_unique_spec UNIQUE(ride_model_id, spec_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_ride_model_technical_specifications_ride_model_id ON ride_model_technical_specifications(ride_model_id);
|
||||||
|
CREATE INDEX idx_ride_model_technical_specifications_category ON ride_model_technical_specifications(category);
|
||||||
|
|
||||||
|
-- Enable RLS on all tables
|
||||||
|
ALTER TABLE ride_technical_specifications ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE ride_coaster_statistics ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE ride_name_history ENABLE ROW LEVEL SECURITY;
|
||||||
|
ALTER TABLE ride_model_technical_specifications ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- RLS Policies for ride_technical_specifications
|
||||||
|
CREATE POLICY "Public read access to ride technical specifications"
|
||||||
|
ON ride_technical_specifications FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Moderators can manage ride technical specifications"
|
||||||
|
ON ride_technical_specifications FOR ALL
|
||||||
|
USING (is_moderator(auth.uid()));
|
||||||
|
|
||||||
|
-- RLS Policies for ride_coaster_statistics
|
||||||
|
CREATE POLICY "Public read access to ride coaster statistics"
|
||||||
|
ON ride_coaster_statistics FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Moderators can manage ride coaster statistics"
|
||||||
|
ON ride_coaster_statistics FOR ALL
|
||||||
|
USING (is_moderator(auth.uid()));
|
||||||
|
|
||||||
|
-- RLS Policies for ride_name_history
|
||||||
|
CREATE POLICY "Public read access to ride name history"
|
||||||
|
ON ride_name_history FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Moderators can manage ride name history"
|
||||||
|
ON ride_name_history FOR ALL
|
||||||
|
USING (is_moderator(auth.uid()));
|
||||||
|
|
||||||
|
-- RLS Policies for ride_model_technical_specifications
|
||||||
|
CREATE POLICY "Public read access to ride model technical specifications"
|
||||||
|
ON ride_model_technical_specifications FOR SELECT
|
||||||
|
USING (true);
|
||||||
|
|
||||||
|
CREATE POLICY "Moderators can manage ride model technical specifications"
|
||||||
|
ON ride_model_technical_specifications FOR ALL
|
||||||
|
USING (is_moderator(auth.uid()));
|
||||||
|
|
||||||
|
-- Data Migration Function: Migrate existing JSON data to relational tables
|
||||||
|
CREATE OR REPLACE FUNCTION migrate_ride_technical_data()
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
ride_record RECORD;
|
||||||
|
spec_key TEXT;
|
||||||
|
spec_value TEXT;
|
||||||
|
stat_key TEXT;
|
||||||
|
stat_value NUMERIC;
|
||||||
|
former_name_item JSONB;
|
||||||
|
spec_order INTEGER;
|
||||||
|
stat_order INTEGER;
|
||||||
|
name_order INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Migrate technical_specs from rides
|
||||||
|
FOR ride_record IN
|
||||||
|
SELECT id, technical_specs
|
||||||
|
FROM rides
|
||||||
|
WHERE technical_specs IS NOT NULL AND technical_specs != 'null'::jsonb
|
||||||
|
LOOP
|
||||||
|
spec_order := 0;
|
||||||
|
FOR spec_key, spec_value IN
|
||||||
|
SELECT key, value::text
|
||||||
|
FROM jsonb_each_text(ride_record.technical_specs)
|
||||||
|
LOOP
|
||||||
|
INSERT INTO ride_technical_specifications (
|
||||||
|
ride_id,
|
||||||
|
spec_name,
|
||||||
|
spec_value,
|
||||||
|
spec_type,
|
||||||
|
display_order
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
ride_record.id,
|
||||||
|
spec_key,
|
||||||
|
spec_value,
|
||||||
|
CASE
|
||||||
|
WHEN spec_value ~ '^[0-9]+\.?[0-9]*$' THEN 'number'
|
||||||
|
WHEN spec_value IN ('true', 'false') THEN 'boolean'
|
||||||
|
ELSE 'string'
|
||||||
|
END,
|
||||||
|
spec_order
|
||||||
|
)
|
||||||
|
ON CONFLICT (ride_id, spec_name) DO NOTHING;
|
||||||
|
|
||||||
|
spec_order := spec_order + 1;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Migrate coaster_stats from rides
|
||||||
|
FOR ride_record IN
|
||||||
|
SELECT id, coaster_stats
|
||||||
|
FROM rides
|
||||||
|
WHERE coaster_stats IS NOT NULL AND coaster_stats != 'null'::jsonb
|
||||||
|
LOOP
|
||||||
|
stat_order := 0;
|
||||||
|
FOR stat_key, stat_value IN
|
||||||
|
SELECT key, (value::text)::numeric
|
||||||
|
FROM jsonb_each(ride_record.coaster_stats)
|
||||||
|
WHERE value::text ~ '^[0-9]+\.?[0-9]*$'
|
||||||
|
LOOP
|
||||||
|
INSERT INTO ride_coaster_statistics (
|
||||||
|
ride_id,
|
||||||
|
stat_name,
|
||||||
|
stat_value,
|
||||||
|
display_order
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
ride_record.id,
|
||||||
|
stat_key,
|
||||||
|
stat_value,
|
||||||
|
stat_order
|
||||||
|
)
|
||||||
|
ON CONFLICT (ride_id, stat_name) DO NOTHING;
|
||||||
|
|
||||||
|
stat_order := stat_order + 1;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Migrate former_names from rides
|
||||||
|
FOR ride_record IN
|
||||||
|
SELECT id, former_names
|
||||||
|
FROM rides
|
||||||
|
WHERE former_names IS NOT NULL AND former_names != 'null'::jsonb AND jsonb_array_length(former_names) > 0
|
||||||
|
LOOP
|
||||||
|
name_order := 0;
|
||||||
|
FOR former_name_item IN
|
||||||
|
SELECT value
|
||||||
|
FROM jsonb_array_elements(ride_record.former_names)
|
||||||
|
LOOP
|
||||||
|
INSERT INTO ride_name_history (
|
||||||
|
ride_id,
|
||||||
|
former_name,
|
||||||
|
date_changed,
|
||||||
|
reason,
|
||||||
|
order_index
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
ride_record.id,
|
||||||
|
former_name_item->>'name',
|
||||||
|
(former_name_item->>'date')::date,
|
||||||
|
former_name_item->>'reason',
|
||||||
|
name_order
|
||||||
|
);
|
||||||
|
|
||||||
|
name_order := name_order + 1;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- Migrate technical_specs from ride_models
|
||||||
|
FOR ride_record IN
|
||||||
|
SELECT id, technical_specs
|
||||||
|
FROM ride_models
|
||||||
|
WHERE technical_specs IS NOT NULL AND technical_specs != 'null'::jsonb
|
||||||
|
LOOP
|
||||||
|
spec_order := 0;
|
||||||
|
FOR spec_key, spec_value IN
|
||||||
|
SELECT key, value::text
|
||||||
|
FROM jsonb_each_text(ride_record.technical_specs)
|
||||||
|
LOOP
|
||||||
|
INSERT INTO ride_model_technical_specifications (
|
||||||
|
ride_model_id,
|
||||||
|
spec_name,
|
||||||
|
spec_value,
|
||||||
|
spec_type,
|
||||||
|
display_order
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
ride_record.id,
|
||||||
|
spec_key,
|
||||||
|
spec_value,
|
||||||
|
CASE
|
||||||
|
WHEN spec_value ~ '^[0-9]+\.?[0-9]*$' THEN 'number'
|
||||||
|
WHEN spec_value IN ('true', 'false') THEN 'boolean'
|
||||||
|
ELSE 'string'
|
||||||
|
END,
|
||||||
|
spec_order
|
||||||
|
)
|
||||||
|
ON CONFLICT (ride_model_id, spec_name) DO NOTHING;
|
||||||
|
|
||||||
|
spec_order := spec_order + 1;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Data migration completed successfully';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Execute the migration
|
||||||
|
SELECT migrate_ride_technical_data();
|
||||||
Reference in New Issue
Block a user