From 7b6dfc47412b15b02fe17914c89903683d7287d6 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:00:33 +0000 Subject: [PATCH 001/477] Fix moderation queue query --- src/lib/moderation/queries.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/moderation/queries.ts b/src/lib/moderation/queries.ts index 7341d750..7276476d 100644 --- a/src/lib/moderation/queries.ts +++ b/src/lib/moderation/queries.ts @@ -137,8 +137,9 @@ export function buildSubmissionQuery( // Admins see all submissions if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); + // Fixed: Add null check for locked_until to prevent 400 error query = query.or( - `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` + `assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}` ); } @@ -189,8 +190,9 @@ export function buildCountQuery( if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); + // Fixed: Add null check for locked_until to prevent 400 error countQuery = countQuery.or( - `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` + `assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}` ); } From 3e0c4db0a17797da9eeabfffe2f8eb51816468a5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:09:45 +0000 Subject: [PATCH 002/477] Refactor moderation queue query --- src/lib/moderation/queries.ts | 164 +++++++++++++++++++++++++++++----- 1 file changed, 143 insertions(+), 21 deletions(-) diff --git a/src/lib/moderation/queries.ts b/src/lib/moderation/queries.ts index 7276476d..ce22d73b 100644 --- a/src/lib/moderation/queries.ts +++ b/src/lib/moderation/queries.ts @@ -45,11 +45,13 @@ export interface FetchSubmissionsResult { * * @param supabase - Supabase client instance * @param config - Query configuration + * @param skipModeratorFilter - Skip the moderator access control filter * @returns Configured Supabase query builder */ export function buildSubmissionQuery( supabase: SupabaseClient, - config: QueryConfig + config: QueryConfig, + skipModeratorFilter = false ) { const { entityFilter, statusFilter, tab, userId, isAdmin, isSuperuser } = config; @@ -135,11 +137,12 @@ export function buildSubmissionQuery( // CRM-style claim filtering: moderators only see unclaimed OR self-assigned submissions // Admins see all submissions - if (!isAdmin && !isSuperuser) { + // Note: For non-admin users, moderator filtering is handled by multi-query approach in fetchSubmissions + if (!isAdmin && !isSuperuser && !skipModeratorFilter) { const now = new Date().toISOString(); - // Fixed: Add null check for locked_until to prevent 400 error + // Single filter approach (used by getQueueStats) query = query.or( - `assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}` + `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` ); } @@ -188,11 +191,11 @@ export function buildCountQuery( countQuery = countQuery.neq('submission_type', 'photo'); } + // Note: Count query not used for non-admin users (multi-query approach handles count) if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); - // Fixed: Add null check for locked_until to prevent 400 error countQuery = countQuery.or( - `assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}` + `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` ); } @@ -228,7 +231,14 @@ export async function fetchSubmissions( config: QueryConfig ): Promise { try { - // Get total count first + const { userId, isAdmin, isSuperuser, currentPage, pageSize } = config; + + // For non-admin users, use multi-query approach to avoid complex OR filters + if (!isAdmin && !isSuperuser) { + return await fetchSubmissionsMultiQuery(supabase, config); + } + + // Admin path: use single query with count const countQuery = buildCountQuery(supabase, config); const { count, error: countError } = await countQuery; @@ -238,8 +248,8 @@ export async function fetchSubmissions( // Build main query with pagination const query = buildSubmissionQuery(supabase, config); - const startIndex = (config.currentPage - 1) * config.pageSize; - const endIndex = startIndex + config.pageSize - 1; + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize - 1; const paginatedQuery = query.range(startIndex, endIndex); // Execute query @@ -249,19 +259,130 @@ export async function fetchSubmissions( throw submissionsError; } - // Enrich submissions with type field for UI conditional logic - const enrichedSubmissions = (submissions || []).map(sub => ({ - ...sub, - type: 'content_submission' as const, - })); + // Enrich submissions with type field for UI conditional logic + const enrichedSubmissions = (submissions || []).map(sub => ({ + ...sub, + type: 'content_submission' as const, + })); - return { - submissions: enrichedSubmissions, - totalCount: count || 0, - }; + return { + submissions: enrichedSubmissions, + totalCount: count || 0, + }; + } catch (error: unknown) { + return { + submissions: [], + totalCount: 0, + error: error as Error, + }; + } +} + +/** + * Fetch submissions using multi-query approach for non-admin users + * + * Executes three separate queries to avoid complex OR filters: + * 1. Unclaimed items (assigned_to is null) + * 2. Expired locks (locked_until < now, not assigned to current user) + * 3. Items assigned to current user + * + * Results are merged, deduplicated, sorted, and paginated. + */ +async function fetchSubmissionsMultiQuery( + supabase: SupabaseClient, + config: QueryConfig +): Promise { + const { userId, currentPage, pageSize } = config; + const now = new Date().toISOString(); + + try { + // Build three separate queries + // Query 1: Unclaimed items + const query1 = buildSubmissionQuery(supabase, config, true).is('assigned_to', null); + + // Query 2: Expired locks (not mine) + const query2 = buildSubmissionQuery(supabase, config, true) + .not('assigned_to', 'is', null) + .neq('assigned_to', userId) + .lt('locked_until', now); + + // Query 3: My claimed items + const query3 = buildSubmissionQuery(supabase, config, true).eq('assigned_to', userId); + + // Execute all queries in parallel + const [result1, result2, result3] = await Promise.all([ + query1, + query2, + query3, + ]); + + // Check for errors + if (result1.error) throw result1.error; + if (result2.error) throw result2.error; + if (result3.error) throw result3.error; + + // Merge all submissions + const allSubmissions = [ + ...(result1.data || []), + ...(result2.data || []), + ...(result3.data || []), + ]; + + // Deduplicate by ID + const uniqueMap = new Map(); + allSubmissions.forEach(sub => { + if (!uniqueMap.has(sub.id)) { + uniqueMap.set(sub.id, sub); + } + }); + const uniqueSubmissions = Array.from(uniqueMap.values()); + + // Apply sorting (same logic as buildSubmissionQuery) + uniqueSubmissions.sort((a, b) => { + // Level 1: Escalated first + if (a.escalated !== b.escalated) { + return b.escalated ? 1 : -1; + } + + // Level 2: Custom sort (if provided) + if (config.sortConfig) { + const field = config.sortConfig.field; + const ascending = config.sortConfig.direction === 'asc'; + const aVal = a[field]; + const bVal = b[field]; + + if (aVal !== bVal) { + if (aVal == null) return 1; + if (bVal == null) return -1; + + const comparison = aVal < bVal ? -1 : 1; + return ascending ? comparison : -comparison; + } + } + + // Level 3: Tiebreaker by created_at + const aTime = new Date(a.created_at).getTime(); + const bTime = new Date(b.created_at).getTime(); + return aTime - bTime; + }); + + // Apply pagination + const totalCount = uniqueSubmissions.length; + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const paginatedSubmissions = uniqueSubmissions.slice(startIndex, endIndex); + + // Enrich with type field + const enrichedSubmissions = paginatedSubmissions.map(sub => ({ + ...sub, + type: 'content_submission' as const, + })); + + return { + submissions: enrichedSubmissions, + totalCount, + }; } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : String(error); - // Use logger instead of console.error for consistent error tracking return { submissions: [], totalCount: 0, @@ -327,9 +448,10 @@ export async function getQueueStats( .from('content_submissions') .select('status, escalated'); - // Apply access control + // Apply access control using simple OR filter if (!isAdmin && !isSuperuser) { const now = new Date().toISOString(); + // Show: unclaimed items OR items with expired locks OR my items statsQuery = statsQuery.or( `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` ); From 32354548ceab220b7cf0bfd5cc8e9a550c1d5fdf Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:23:45 +0000 Subject: [PATCH 003/477] Fix foreign key constraints --- src/integrations/supabase/types.ts | 56 +++++++++++++++++++ ...4_a9914cea-ac16-4114-8c20-07f91f0e57bb.sql | 37 ++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 supabase/migrations/20251029132334_a9914cea-ac16-4114-8c20-07f91f0e57bb.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 6631f394..cf129f4a 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -692,6 +692,34 @@ export type Database = { user_id?: string } Relationships: [ + { + foreignKeyName: "content_submissions_assigned_to_fkey" + columns: ["assigned_to"] + isOneToOne: false + referencedRelation: "filtered_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_assigned_to_fkey" + columns: ["assigned_to"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_escalated_by_fkey" + columns: ["escalated_by"] + isOneToOne: false + referencedRelation: "filtered_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_escalated_by_fkey" + columns: ["escalated_by"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["user_id"] + }, { foreignKeyName: "content_submissions_original_submission_id_fkey" columns: ["original_submission_id"] @@ -699,6 +727,34 @@ export type Database = { referencedRelation: "content_submissions" referencedColumns: ["id"] }, + { + foreignKeyName: "content_submissions_reviewer_id_fkey" + columns: ["reviewer_id"] + isOneToOne: false + referencedRelation: "filtered_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_reviewer_id_fkey" + columns: ["reviewer_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "filtered_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "content_submissions_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "profiles" + referencedColumns: ["user_id"] + }, ] } email_aliases: { diff --git a/supabase/migrations/20251029132334_a9914cea-ac16-4114-8c20-07f91f0e57bb.sql b/supabase/migrations/20251029132334_a9914cea-ac16-4114-8c20-07f91f0e57bb.sql new file mode 100644 index 00000000..6a540e4d --- /dev/null +++ b/supabase/migrations/20251029132334_a9914cea-ac16-4114-8c20-07f91f0e57bb.sql @@ -0,0 +1,37 @@ +-- Drop existing foreign keys that point to auth.users +ALTER TABLE public.content_submissions +DROP CONSTRAINT IF EXISTS content_submissions_user_id_fkey; + +ALTER TABLE public.content_submissions +DROP CONSTRAINT IF EXISTS content_submissions_reviewer_id_fkey; + +ALTER TABLE public.content_submissions +DROP CONSTRAINT IF EXISTS content_submissions_assigned_to_fkey; + +ALTER TABLE public.content_submissions +DROP CONSTRAINT IF EXISTS content_submissions_escalated_by_fkey; + +-- Create new foreign keys pointing to profiles table +ALTER TABLE public.content_submissions +ADD CONSTRAINT content_submissions_user_id_fkey +FOREIGN KEY (user_id) +REFERENCES public.profiles(user_id) +ON DELETE CASCADE; + +ALTER TABLE public.content_submissions +ADD CONSTRAINT content_submissions_reviewer_id_fkey +FOREIGN KEY (reviewer_id) +REFERENCES public.profiles(user_id) +ON DELETE SET NULL; + +ALTER TABLE public.content_submissions +ADD CONSTRAINT content_submissions_assigned_to_fkey +FOREIGN KEY (assigned_to) +REFERENCES public.profiles(user_id) +ON DELETE SET NULL; + +ALTER TABLE public.content_submissions +ADD CONSTRAINT content_submissions_escalated_by_fkey +FOREIGN KEY (escalated_by) +REFERENCES public.profiles(user_id) +ON DELETE SET NULL; \ No newline at end of file From a38be4221e281fd7de86c027ad779493ff240a71 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:46:55 +0000 Subject: [PATCH 004/477] Fix: Disable triggers for database purge --- ...4_f7178e15-82dc-460f-b170-b1a7609402b7.sql | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 supabase/migrations/20251029134644_f7178e15-82dc-460f-b170-b1a7609402b7.sql diff --git a/supabase/migrations/20251029134644_f7178e15-82dc-460f-b170-b1a7609402b7.sql b/supabase/migrations/20251029134644_f7178e15-82dc-460f-b170-b1a7609402b7.sql new file mode 100644 index 00000000..4679c664 --- /dev/null +++ b/supabase/migrations/20251029134644_f7178e15-82dc-460f-b170-b1a7609402b7.sql @@ -0,0 +1,78 @@ +-- Full Database Purge (Keep Users Only) - Disable Triggers +-- WARNING: This will delete ALL content data while preserving user accounts + +-- Disable all triggers temporarily +SET session_replication_role = replica; + +-- 1. Clear submission items first (depends on content_submissions) +DELETE FROM public.submission_items; + +-- 2. Clear timeline events (has FK to content_submissions) +DELETE FROM public.entity_timeline_events; + +-- 3. Clear specific submission type tables +DELETE FROM public.park_submissions; +DELETE FROM public.ride_submissions; +DELETE FROM public.company_submissions; +DELETE FROM public.ride_model_submissions; + +-- 4. Clear main content submissions (now safe) +DELETE FROM public.content_submissions; + +-- 5. Clear user-generated content +DELETE FROM public.reviews; +DELETE FROM public.reports; +DELETE FROM public.photos; + +-- 6. Clear user lists +DELETE FROM public.list_items; +DELETE FROM public.user_top_lists; + +-- 7. Clear user ride tracking +DELETE FROM public.user_ride_credits; + +-- 8. Clear historical records +DELETE FROM public.historical_rides; +DELETE FROM public.historical_parks; + +-- 9. Clear entity versions (history) +DELETE FROM public.ride_versions; +DELETE FROM public.park_versions; +DELETE FROM public.company_versions; +DELETE FROM public.ride_model_versions; +DELETE FROM public.entity_versions_archive; + +-- 10. Clear main entity tables (no dependencies left) +DELETE FROM public.rides; +DELETE FROM public.parks; +DELETE FROM public.companies; +DELETE FROM public.ride_models; + +-- 11. Clear park-related tables +DELETE FROM public.park_location_history; +DELETE FROM public.park_operating_hours; + +-- 12. Clear analytics/tracking +DELETE FROM public.entity_page_views; + +-- 13. Clear contact/support +DELETE FROM public.contact_email_threads; +DELETE FROM public.contact_submissions; + +-- 14. Clear notification logs +DELETE FROM public.notification_logs; + +-- Re-enable all triggers +SET session_replication_role = DEFAULT; + +-- Verify user data remains intact +DO $$ +DECLARE + user_count INTEGER; + profile_count INTEGER; +BEGIN + SELECT COUNT(*) INTO user_count FROM auth.users; + SELECT COUNT(*) INTO profile_count FROM public.profiles; + + RAISE NOTICE 'Database purge complete. Users preserved: %, Profiles: %', user_count, profile_count; +END $$; \ No newline at end of file From ed205e68cd003b25f7b4f37868640796f423700c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:56:59 +0000 Subject: [PATCH 005/477] feat: Add operator is owner checkbox --- src/components/admin/ParkForm.tsx | 62 +++++++++++++++++++++++++----- src/lib/entityValidationSchemas.ts | 4 +- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index dc2c1de6..5e51420b 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -29,6 +29,7 @@ import type { TempCompanyData } from '@/types/company'; import { LocationSearch } from './LocationSearch'; import { OperatorForm } from './OperatorForm'; import { PropertyOwnerForm } from './PropertyOwnerForm'; +import { Checkbox } from '@/components/ui/checkbox'; const parkSchema = z.object({ name: z.string().min(1, 'Park name is required'), @@ -55,8 +56,8 @@ const parkSchema = z.object({ website_url: z.string().url().optional().or(z.literal('')), phone: z.string().optional(), email: z.string().email().optional().or(z.literal('')), - operator_id: z.string().uuid().optional(), - property_owner_id: z.string().uuid().optional(), + operator_id: z.string().uuid().optional().or(z.literal('')).transform(val => val || undefined), + property_owner_id: z.string().uuid().optional().or(z.literal('')).transform(val => val || undefined), images: z.object({ uploaded: z.array(z.object({ url: z.string(), @@ -148,6 +149,12 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: const [tempNewPropertyOwner, setTempNewPropertyOwner] = useState(null); const [isPropertyOwnerModalOpen, setIsPropertyOwnerModalOpen] = useState(false); + // Operator is Owner checkbox state + const [operatorIsOwner, setOperatorIsOwner] = useState( + !!(initialData?.operator_id && initialData?.property_owner_id && + initialData?.operator_id === initialData?.property_owner_id) + ); + // Fetch data const { operators, loading: operatorsLoading } = useOperators(); const { propertyOwners, loading: ownersLoading } = usePropertyOwners(); @@ -178,6 +185,14 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: } }); + // Sync property owner with operator when checkbox is enabled + useEffect(() => { + if (operatorIsOwner && selectedOperatorId) { + setSelectedPropertyOwnerId(selectedOperatorId); + setValue('property_owner_id', selectedOperatorId); + } + }, [operatorIsOwner, selectedOperatorId, setValue]); + const handleFormSubmit = async (data: ParkFormData) => { try { @@ -198,10 +213,15 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: submissionContent.park.property_owner_id = null; } + const finalOperatorId = tempNewOperator ? undefined : (selectedOperatorId || undefined); + const finalPropertyOwnerId = operatorIsOwner + ? finalOperatorId + : (tempNewPropertyOwner ? undefined : (selectedPropertyOwnerId || undefined)); + await onSubmit({ ...data, - operator_id: tempNewOperator ? undefined : (selectedOperatorId || undefined), - property_owner_id: tempNewPropertyOwner ? undefined : (selectedPropertyOwnerId || undefined), + operator_id: finalOperatorId, + property_owner_id: finalPropertyOwnerId, _compositeSubmission: (tempNewOperator || tempNewPropertyOwner) ? submissionContent : undefined }); @@ -363,6 +383,24 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:

Operator & Property Owner

+
+ { + setOperatorIsOwner(checked as boolean); + if (checked && selectedOperatorId) { + setSelectedPropertyOwnerId(selectedOperatorId); + setValue('property_owner_id', selectedOperatorId); + setTempNewPropertyOwner(null); + } + }} + /> + +
+
{/* Operator Column */}
@@ -384,10 +422,11 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: ) : ( { - setValue('operator_id', value); - setSelectedOperatorId(value); + const cleanValue = value || undefined; + setValue('operator_id', cleanValue); + setSelectedOperatorId(cleanValue || ''); }} placeholder="Select operator" searchPlaceholder="Search operators..." @@ -411,6 +450,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
{/* Property Owner Column */} + {!operatorIsOwner && (
@@ -430,10 +470,11 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: ) : ( { - setValue('property_owner_id', value); - setSelectedPropertyOwnerId(value); + const cleanValue = value || undefined; + setValue('property_owner_id', cleanValue); + setSelectedPropertyOwnerId(cleanValue || ''); }} placeholder="Select property owner" searchPlaceholder="Search property owners..." @@ -455,6 +496,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: )}
+ )}
diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts index f5391141..4f4f3d1d 100644 --- a/src/lib/entityValidationSchemas.ts +++ b/src/lib/entityValidationSchemas.ts @@ -46,8 +46,8 @@ export const parkValidationSchema = z.object({ if (!val || val === '') return true; return z.string().email().safeParse(val).success; }, 'Invalid email format'), - operator_id: z.string().uuid().optional().nullable(), - property_owner_id: z.string().uuid().optional().nullable(), + operator_id: z.string().uuid().optional().nullable().or(z.literal('')).transform(val => val || undefined), + property_owner_id: z.string().uuid().optional().nullable().or(z.literal('')).transform(val => val || undefined), banner_image_id: z.string().optional(), banner_image_url: z.string().optional(), card_image_id: z.string().optional(), From 107191c12551344deb078a070674ee837fc560e5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:00:59 +0000 Subject: [PATCH 006/477] feat: Add HeadquartersLocationInput component --- src/components/admin/DesignerForm.tsx | 17 +- .../admin/HeadquartersLocationInput.tsx | 188 ++++++++++++++++++ src/components/admin/ManufacturerForm.tsx | 17 +- src/components/admin/OperatorForm.tsx | 17 +- src/components/admin/PropertyOwnerForm.tsx | 17 +- src/components/admin/index.ts | 1 + 6 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 src/components/admin/HeadquartersLocationInput.tsx diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index 9291f665..bc72b0e9 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -12,9 +12,8 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { SlugField } from '@/components/ui/slug-field'; import { Ruler, Save, X } from 'lucide-react'; -import { Combobox } from '@/components/ui/combobox'; -import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; +import { HeadquartersLocationInput } from './HeadquartersLocationInput'; import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input'; import { submitDesignerCreation, submitDesignerUpdate } from '@/lib/entitySubmissionHelpers'; @@ -58,7 +57,6 @@ interface DesignerFormProps { export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormProps) { const { isModerator } = useUserRole(); - const { headquarters } = useCompanyHeadquarters(); const { user } = useAuth(); const navigate = useNavigate(); @@ -199,14 +197,13 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
- setValue('headquarters_location', value)} - placeholder="Select or type location" - searchPlaceholder="Search locations..." - emptyText="No locations found" + setValue('headquarters_location', value)} /> +

+ Search OpenStreetMap for accurate location data, or manually enter location name. +

diff --git a/src/components/admin/HeadquartersLocationInput.tsx b/src/components/admin/HeadquartersLocationInput.tsx new file mode 100644 index 00000000..4bf4193c --- /dev/null +++ b/src/components/admin/HeadquartersLocationInput.tsx @@ -0,0 +1,188 @@ +import { useState, useEffect } from 'react'; +import { Input } from '@/components/ui/input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Search, Edit, MapPin, Loader2, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface LocationResult { + place_id: number; + display_name: string; + address?: { + city?: string; + town?: string; + village?: string; + state?: string; + country?: string; + }; +} + +interface HeadquartersLocationInputProps { + value: string; + onChange: (value: string) => void; + disabled?: boolean; + className?: string; +} + +export function HeadquartersLocationInput({ + value, + onChange, + disabled = false, + className +}: HeadquartersLocationInputProps) { + const [mode, setMode] = useState<'search' | 'manual'>('search'); + const [searchQuery, setSearchQuery] = useState(''); + const [results, setResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [showResults, setShowResults] = useState(false); + + // Debounced search effect + useEffect(() => { + if (!searchQuery || searchQuery.length < 2) { + setResults([]); + setShowResults(false); + return; + } + + const timeoutId = setTimeout(async () => { + setIsSearching(true); + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent( + searchQuery + )}&limit=5&addressdetails=1`, + { + headers: { + 'User-Agent': 'ThemeParkArchive/1.0' + } + } + ); + + if (response.ok) { + const data = await response.json(); + setResults(data); + setShowResults(true); + } + } catch (error) { + console.error('Error searching locations:', error); + } finally { + setIsSearching(false); + } + }, 500); + + return () => clearTimeout(timeoutId); + }, [searchQuery]); + + const formatLocation = (result: LocationResult): string => { + const { city, town, village, state, country } = result.address || {}; + const cityName = city || town || village; + + if (cityName && state && country) { + return `${cityName}, ${state}, ${country}`; + } else if (cityName && country) { + return `${cityName}, ${country}`; + } else if (country) { + return country; + } + return result.display_name; + }; + + const handleSelectLocation = (result: LocationResult) => { + const formatted = formatLocation(result); + onChange(formatted); + setSearchQuery(''); + setShowResults(false); + setResults([]); + }; + + const handleClear = () => { + onChange(''); + setSearchQuery(''); + setResults([]); + setShowResults(false); + }; + + return ( +
+ setMode(val as 'search' | 'manual')}> + + + + Search Location + + + + Manual Entry + + + + +
+ setSearchQuery(e.target.value)} + placeholder="Search for location (e.g., Munich, Germany)..." + disabled={disabled} + className="pr-10" + /> + {isSearching && ( + + )} +
+ + {showResults && results.length > 0 && ( +
+ {results.map((result) => ( + + ))} +
+ )} + + {showResults && results.length === 0 && !isSearching && ( +

+ No locations found. Try a different search term. +

+ )} + + {value && ( +
+ + {value} + +
+ )} +
+ + + onChange(e.target.value)} + placeholder="Enter location manually..." + disabled={disabled} + /> +

+ Enter any location text. For better data quality, use Search mode. +

+
+
+
+ ); +} diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index 48e8a8c5..37dd07b7 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -12,9 +12,8 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { SlugField } from '@/components/ui/slug-field'; import { Building2, Save, X } from 'lucide-react'; -import { Combobox } from '@/components/ui/combobox'; -import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; +import { HeadquartersLocationInput } from './HeadquartersLocationInput'; import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input'; import { submitManufacturerCreation, submitManufacturerUpdate } from '@/lib/entitySubmissionHelpers'; @@ -59,7 +58,6 @@ interface ManufacturerFormProps { export function ManufacturerForm({ onSubmit, onCancel, initialData }: ManufacturerFormProps) { const { isModerator } = useUserRole(); - const { headquarters } = useCompanyHeadquarters(); const { user } = useAuth(); const navigate = useNavigate(); @@ -200,14 +198,13 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
- setValue('headquarters_location', value)} - placeholder="Select or type location" - searchPlaceholder="Search locations..." - emptyText="No locations found" + setValue('headquarters_location', value)} /> +

+ Search OpenStreetMap for accurate location data, or manually enter location name. +

diff --git a/src/components/admin/OperatorForm.tsx b/src/components/admin/OperatorForm.tsx index d90a20f0..1a0dd24f 100644 --- a/src/components/admin/OperatorForm.tsx +++ b/src/components/admin/OperatorForm.tsx @@ -12,9 +12,8 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { SlugField } from '@/components/ui/slug-field'; import { FerrisWheel, Save, X } from 'lucide-react'; -import { Combobox } from '@/components/ui/combobox'; -import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; +import { HeadquartersLocationInput } from './HeadquartersLocationInput'; import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input'; import { submitOperatorCreation, submitOperatorUpdate } from '@/lib/entitySubmissionHelpers'; @@ -58,7 +57,6 @@ interface OperatorFormProps { export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormProps) { const { isModerator } = useUserRole(); - const { headquarters } = useCompanyHeadquarters(); const { user } = useAuth(); const navigate = useNavigate(); @@ -199,14 +197,13 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
- setValue('headquarters_location', value)} - placeholder="Select or type location" - searchPlaceholder="Search locations..." - emptyText="No locations found" + setValue('headquarters_location', value)} /> +

+ Search OpenStreetMap for accurate location data, or manually enter location name. +

diff --git a/src/components/admin/PropertyOwnerForm.tsx b/src/components/admin/PropertyOwnerForm.tsx index afd2207c..b2edebca 100644 --- a/src/components/admin/PropertyOwnerForm.tsx +++ b/src/components/admin/PropertyOwnerForm.tsx @@ -12,9 +12,8 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { SlugField } from '@/components/ui/slug-field'; import { Building2, Save, X } from 'lucide-react'; -import { Combobox } from '@/components/ui/combobox'; -import { useCompanyHeadquarters } from '@/hooks/useAutocompleteData'; import { useUserRole } from '@/hooks/useUserRole'; +import { HeadquartersLocationInput } from './HeadquartersLocationInput'; import { EntityMultiImageUploader, ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible-date-input'; import { submitPropertyOwnerCreation, submitPropertyOwnerUpdate } from '@/lib/entitySubmissionHelpers'; @@ -58,7 +57,6 @@ interface PropertyOwnerFormProps { export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyOwnerFormProps) { const { isModerator } = useUserRole(); - const { headquarters } = useCompanyHeadquarters(); const { user } = useAuth(); const navigate = useNavigate(); @@ -199,14 +197,13 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
- setValue('headquarters_location', value)} - placeholder="Select or type location" - searchPlaceholder="Search locations..." - emptyText="No locations found" + setValue('headquarters_location', value)} /> +

+ Search OpenStreetMap for accurate location data, or manually enter location name. +

diff --git a/src/components/admin/index.ts b/src/components/admin/index.ts index 429fae97..c9642531 100644 --- a/src/components/admin/index.ts +++ b/src/components/admin/index.ts @@ -1,6 +1,7 @@ // Admin components barrel exports export { AdminPageLayout } from './AdminPageLayout'; export { DesignerForm } from './DesignerForm'; +export { HeadquartersLocationInput } from './HeadquartersLocationInput'; export { LocationSearch } from './LocationSearch'; export { ManufacturerForm } from './ManufacturerForm'; export { MarkdownEditor } from './MarkdownEditor'; From 9aaff5a10b2ce3617ea55d7e3f223edf8bf046d5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:30:15 +0000 Subject: [PATCH 007/477] feat: Add submitter fields to entity forms --- src/components/admin/DesignerForm.tsx | 60 +++++++++++++++++ src/components/admin/ManufacturerForm.tsx | 60 +++++++++++++++++ src/components/admin/OperatorForm.tsx | 60 +++++++++++++++++ src/components/admin/ParkForm.tsx | 59 +++++++++++++++++ src/components/admin/PropertyOwnerForm.tsx | 60 +++++++++++++++++ src/components/admin/RideForm.tsx | 57 ++++++++++++++++ src/components/admin/RideModelForm.tsx | 59 +++++++++++++++++ src/components/moderation/QueueItem.tsx | 77 +++++++++++++++++++++- src/lib/entityValidationSchemas.ts | 32 +++++++++ src/types/submission-data.ts | 8 +++ 10 files changed, 529 insertions(+), 3 deletions(-) diff --git a/src/components/admin/DesignerForm.tsx b/src/components/admin/DesignerForm.tsx index bc72b0e9..d3c1dab7 100644 --- a/src/components/admin/DesignerForm.tsx +++ b/src/components/admin/DesignerForm.tsx @@ -10,6 +10,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; import { SlugField } from '@/components/ui/slug-field'; import { Ruler, Save, X } from 'lucide-react'; import { useUserRole } from '@/hooks/useUserRole'; @@ -35,6 +36,8 @@ interface DesignerFormInput { founded_date_precision?: 'day' | 'month' | 'year'; headquarters_location?: string; website_url?: string; + source_url?: string; + submission_notes?: string; images?: { uploaded: UploadedImage[]; banner_assignment?: number | null; @@ -77,6 +80,8 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr website_url: initialData?.website_url || '', founded_year: initialData?.founded_year ? String(initialData.founded_year) : '', headquarters_location: initialData?.headquarters_location || '', + source_url: initialData?.source_url || '', + submission_notes: initialData?.submission_notes || '', images: initialData?.images || { uploaded: [] } } }); @@ -221,6 +226,61 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr )} + {/* Submission Context - For Reviewers */} +
+
+ + For Moderator Review + +

+ Help reviewers verify your submission +

+
+ +
+ + +

+ Where did you find this information? (e.g., official website, news article, press release) +

+ {errors.source_url && ( +

{errors.source_url.message}

+ )} +
+ +
+ +