From d0c613031e87f9811d89ce8dd578b9f803646651 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 22:05:29 +0000 Subject: [PATCH] Migrate date precision to exact Batch update all date precision handling to use expanded DatePrecision, replace hardcoded day defaults, and adjust related validation, UI, and helpers. Includes wrapper migration across Phase 1-3 functions, updates to logs, displays, and formatting utilities to align frontend with new precision values ('exact', 'month', 'year', 'decade', 'century', 'approximate'). --- src/components/admin/ManufacturerForm.tsx | 2 +- src/components/admin/ParkForm.tsx | 4 +- src/components/admin/RideForm.tsx | 4 +- .../TimeZoneIndependentDateRangePicker.tsx | 6 +- src/components/moderation/FieldComparison.tsx | 4 +- .../moderation/SpecialFieldDisplay.tsx | 8 +- .../timeline/TimelineEventEditorDialog.tsx | 9 +- src/components/ui/flexible-date-display.tsx | 13 ++- src/components/ui/flexible-date-input.tsx | 29 ++++-- src/lib/dateUtils.ts | 20 +++- src/lib/entityValidationSchemas.ts | 12 +-- src/lib/submissionChangeDetection.ts | 16 ++- src/pages/ParkDetail.tsx | 4 +- src/types/timeline.ts | 2 +- ...2_59da5009-54b6-4fef-acb2-d75943214f1a.sql | 99 +++++++++++++++++++ 15 files changed, 189 insertions(+), 43 deletions(-) create mode 100644 supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql diff --git a/src/components/admin/ManufacturerForm.tsx b/src/components/admin/ManufacturerForm.tsx index d16fc732..5c0bb3ed 100644 --- a/src/components/admin/ManufacturerForm.tsx +++ b/src/components/admin/ManufacturerForm.tsx @@ -57,7 +57,7 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur website_url: initialData?.website_url || '', founded_year: initialData?.founded_year ? String(initialData.founded_year) : '', founded_date: initialData?.founded_date || (initialData?.founded_year ? `${initialData.founded_year}-01-01` : undefined), - founded_date_precision: initialData?.founded_date_precision || (initialData?.founded_year ? ('year' as const) : ('day' as const)), + founded_date_precision: initialData?.founded_date_precision || (initialData?.founded_year ? ('year' as const) : ('exact' as const)), headquarters_location: initialData?.headquarters_location || '', source_url: initialData?.source_url || '', submission_notes: initialData?.submission_notes || '', diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 3b1c6eb0..60e0a204 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -38,9 +38,9 @@ const parkSchema = z.object({ park_type: z.string().min(1, 'Park type is required'), status: z.string().min(1, 'Status is required'), opening_date: z.string().optional().transform(val => val || undefined), - opening_date_precision: z.enum(['day', 'month', 'year']).optional(), + opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional(), closing_date: z.string().optional().transform(val => val || undefined), - closing_date_precision: z.enum(['day', 'month', 'year']).optional(), + closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional(), location: z.object({ name: z.string(), street_address: z.string().optional(), diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index a573bfcf..4317cf02 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -227,9 +227,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: ride_sub_type: initialData?.ride_sub_type || '', status: initialData?.status || 'operating' as const, // Store DB value directly opening_date: initialData?.opening_date || undefined, - opening_date_precision: initialData?.opening_date_precision || 'day', + opening_date_precision: initialData?.opening_date_precision || 'exact', closing_date: initialData?.closing_date || undefined, - closing_date_precision: initialData?.closing_date_precision || 'day', + closing_date_precision: initialData?.closing_date_precision || 'exact', // Convert metric values to user's preferred unit for display height_requirement: initialData?.height_requirement ? convertValueFromMetric(initialData.height_requirement, getDisplayUnit('cm', measurementSystem), 'cm') diff --git a/src/components/filters/TimeZoneIndependentDateRangePicker.tsx b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx index 370cafe2..c41f84b6 100644 --- a/src/components/filters/TimeZoneIndependentDateRangePicker.tsx +++ b/src/components/filters/TimeZoneIndependentDateRangePicker.tsx @@ -102,11 +102,11 @@ export function TimeZoneIndependentDateRangePicker({ if (!fromDate && !toDate) return null; if (fromDate && toDate) { - return `${formatDateDisplay(fromDate, 'day')} - ${formatDateDisplay(toDate, 'day')}`; + return `${formatDateDisplay(fromDate, 'exact')} - ${formatDateDisplay(toDate, 'exact')}`; } else if (fromDate) { - return `From ${formatDateDisplay(fromDate, 'day')}`; + return `From ${formatDateDisplay(fromDate, 'exact')}`; } else if (toDate) { - return `Until ${formatDateDisplay(toDate, 'day')}`; + return `Until ${formatDateDisplay(toDate, 'exact')}`; } return null; diff --git a/src/components/moderation/FieldComparison.tsx b/src/components/moderation/FieldComparison.tsx index 34b826e4..fe983960 100644 --- a/src/components/moderation/FieldComparison.tsx +++ b/src/components/moderation/FieldComparison.tsx @@ -5,8 +5,10 @@ import { ArrowRight } from 'lucide-react'; import { ArrayFieldDiff } from './ArrayFieldDiff'; import { SpecialFieldDisplay } from './SpecialFieldDisplay'; +import type { DatePrecision } from '@/components/ui/flexible-date-input'; + // Helper to format compact values (truncate long strings) -function formatCompactValue(value: unknown, precision?: 'day' | 'month' | 'year', maxLength = 30): string { +function formatCompactValue(value: unknown, precision?: DatePrecision, maxLength = 30): string { const formatted = formatFieldValue(value, precision); if (formatted.length > maxLength) { return formatted.substring(0, maxLength) + '...'; diff --git a/src/components/moderation/SpecialFieldDisplay.tsx b/src/components/moderation/SpecialFieldDisplay.tsx index b35cf678..92f65a98 100644 --- a/src/components/moderation/SpecialFieldDisplay.tsx +++ b/src/components/moderation/SpecialFieldDisplay.tsx @@ -211,7 +211,13 @@ function DateFieldDisplay({ change, compact }: { change: FieldChange; compact: b {formatFieldName(change.field)} {precision && ( - {precision === 'year' ? 'Year Only' : precision === 'month' ? 'Month & Year' : 'Full Date'} + {precision === 'exact' ? 'Exact Day' : + precision === 'month' ? 'Month & Year' : + precision === 'year' ? 'Year Only' : + precision === 'decade' ? 'Decade' : + precision === 'century' ? 'Century' : + precision === 'approximate' ? 'Approximate' : + 'Full Date'} )} diff --git a/src/components/timeline/TimelineEventEditorDialog.tsx b/src/components/timeline/TimelineEventEditorDialog.tsx index f5d32eb5..c9d54f29 100644 --- a/src/components/timeline/TimelineEventEditorDialog.tsx +++ b/src/components/timeline/TimelineEventEditorDialog.tsx @@ -72,7 +72,7 @@ const timelineEventSchema = z.object({ event_date: z.date({ message: 'Event date is required', }), - event_date_precision: z.enum(['day', 'month', 'year']).default('day'), + event_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).default('exact'), title: z.string().min(1, 'Title is required').max(200, 'Title is too long'), description: z.string().max(1000, 'Description is too long').optional(), @@ -133,7 +133,7 @@ export function TimelineEventEditorDialog({ } : { event_type: 'milestone', event_date: new Date(), - event_date_precision: 'day', + event_date_precision: 'exact', title: '', description: '', }, @@ -319,9 +319,12 @@ export function TimelineEventEditorDialog({ - Exact Day + Exact Day Month Only Year Only + Decade + Century + Approximate diff --git a/src/components/ui/flexible-date-display.tsx b/src/components/ui/flexible-date-display.tsx index cebb174c..a46b3939 100644 --- a/src/components/ui/flexible-date-display.tsx +++ b/src/components/ui/flexible-date-display.tsx @@ -11,7 +11,7 @@ interface FlexibleDateDisplayProps { export function FlexibleDateDisplay({ date, - precision = 'day', + precision = 'exact', fallback = 'Unknown', className }: FlexibleDateDisplayProps) { @@ -36,7 +36,16 @@ export function FlexibleDateDisplay({ case 'month': formatted = format(dateObj, 'MMMM yyyy'); break; - case 'day': + case 'decade': + formatted = `${Math.floor(dateObj.getFullYear() / 10) * 10}s`; + break; + case 'century': + formatted = `${Math.ceil(dateObj.getFullYear() / 100)}th century`; + break; + case 'approximate': + formatted = `circa ${format(dateObj, 'yyyy')}`; + break; + case 'exact': default: formatted = format(dateObj, 'PPP'); break; diff --git a/src/components/ui/flexible-date-input.tsx b/src/components/ui/flexible-date-input.tsx index d3e021fb..2c33bfbb 100644 --- a/src/components/ui/flexible-date-input.tsx +++ b/src/components/ui/flexible-date-input.tsx @@ -16,7 +16,7 @@ import { } from "@/components/ui/select"; import { toDateOnly, toDateWithPrecision } from "@/lib/dateUtils"; -export type DatePrecision = 'day' | 'month' | 'year'; +export type DatePrecision = 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'; interface FlexibleDateInputProps { value?: Date; @@ -34,7 +34,7 @@ interface FlexibleDateInputProps { export function FlexibleDateInput({ value, - precision = 'day', + precision = 'exact', onChange, placeholder = "Select date", disabled = false, @@ -71,13 +71,16 @@ export function FlexibleDateInput({ let newDate: Date; switch (newPrecision) { case 'year': + case 'decade': + case 'century': + case 'approximate': newDate = new Date(year, 0, 1); // January 1st (local timezone) setYearValue(year.toString()); break; case 'month': newDate = new Date(year, month, 1); // 1st of month (local timezone) break; - case 'day': + case 'exact': default: newDate = value; // Keep existing date break; @@ -104,10 +107,13 @@ export function FlexibleDateInput({ const getPlaceholderText = () => { switch (localPrecision) { case 'year': + case 'decade': + case 'century': + case 'approximate': return 'Enter year (e.g., 2005)'; case 'month': return 'Select month and year'; - case 'day': + case 'exact': default: return placeholder; } @@ -119,10 +125,10 @@ export function FlexibleDateInput({
- {localPrecision === 'day' && ( + {(localPrecision === 'exact') && ( onChange(date, 'day')} + onSelect={(date) => onChange(date, 'exact')} placeholder={getPlaceholderText()} disabled={disabled} disableFuture={disableFuture} @@ -143,7 +149,7 @@ export function FlexibleDateInput({ /> )} - {localPrecision === 'year' && ( + {(localPrecision === 'year' || localPrecision === 'decade' || localPrecision === 'century' || localPrecision === 'approximate') && ( - Use Full Date - Use Month/Year - Use Year Only + Exact Day + Month & Year + Year Only + Decade + Century + Approximate
diff --git a/src/lib/dateUtils.ts b/src/lib/dateUtils.ts index 2624811b..2874398e 100644 --- a/src/lib/dateUtils.ts +++ b/src/lib/dateUtils.ts @@ -72,7 +72,7 @@ export function getCurrentDateLocal(): string { */ export function formatDateDisplay( dateString: string | null | undefined, - precision: 'day' | 'month' | 'year' = 'day' + precision: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate' = 'exact' ): string { if (!dateString) return ''; @@ -83,7 +83,13 @@ export function formatDateDisplay( return date.getFullYear().toString(); case 'month': return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); - case 'day': + case 'decade': + return `${Math.floor(date.getFullYear() / 10) * 10}s`; + case 'century': + return `${Math.ceil(date.getFullYear() / 100)}th century`; + case 'approximate': + return `circa ${date.getFullYear()}`; + case 'exact': default: return date.toLocaleDateString('en-US', { year: 'numeric', @@ -182,7 +188,7 @@ export function parseDateForDisplay(date: string | Date): Date { */ export function toDateWithPrecision( date: Date, - precision: 'day' | 'month' | 'year' + precision: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate' ): string { const year = date.getFullYear(); const month = date.getMonth() + 1; @@ -193,7 +199,13 @@ export function toDateWithPrecision( return `${year}-01-01`; case 'month': return `${year}-${String(month).padStart(2, '0')}-01`; - case 'day': + case 'decade': + return `${Math.floor(year / 10) * 10}-01-01`; + case 'century': + return `${Math.floor((year - 1) / 100) * 100 + 1}-01-01`; + case 'approximate': + return `${year}-01-01`; + case 'exact': default: return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; } diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts index 8f4bcb8a..e5486c81 100644 --- a/src/lib/entityValidationSchemas.ts +++ b/src/lib/entityValidationSchemas.ts @@ -51,9 +51,9 @@ export const parkValidationSchema = z.object({ const date = new Date(val); return date <= new Date(); }, 'Opening date cannot be in the future'), - opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(), + opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(), closing_date: z.string().nullish().transform(val => val ?? undefined), - closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(), + closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(), location_id: z.string().uuid().optional().nullable(), location: z.object({ name: z.string(), @@ -139,9 +139,9 @@ export const rideValidationSchema = z.object({ .optional() .nullable(), opening_date: z.string().nullish().transform(val => val ?? undefined), - opening_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(), + opening_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(), closing_date: z.string().nullish().transform(val => val ?? undefined), - closing_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(), + closing_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(), height_requirement: z.preprocess( (val) => val === '' || val === null || val === undefined ? undefined : Number(val), z.number().int().min(0, 'Height requirement must be positive').max(300, 'Height requirement must be less than 300cm').optional() @@ -322,7 +322,7 @@ export const companyValidationSchema = z.object({ description: z.string().trim().max(2000, 'Description must be less than 2000 characters').nullish().transform(val => val ?? undefined), person_type: z.enum(['company', 'individual', 'firm', 'organization']), founded_date: z.string().nullish().transform(val => val ?? undefined), - founded_date_precision: z.enum(['day', 'month', 'year']).nullable().optional(), + founded_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).nullable().optional(), founded_year: z.preprocess( (val) => val === '' || val === null || val === undefined ? undefined : Number(val), z.number().int().min(1800, 'Founded year must be after 1800').max(currentYear, `Founded year cannot be after ${currentYear}`).optional() @@ -401,7 +401,7 @@ export const milestoneValidationSchema = z.object({ fiveYearsFromNow.setFullYear(fiveYearsFromNow.getFullYear() + 5); return date <= fiveYearsFromNow; }, 'Event date cannot be more than 5 years in the future'), - event_date_precision: z.enum(['day', 'month', 'year']).optional().default('day'), + event_date_precision: z.enum(['exact', 'month', 'year', 'decade', 'century', 'approximate']).optional().default('exact'), entity_type: z.string().min(1, 'Entity type is required'), entity_id: z.string().uuid('Invalid entity ID'), is_public: z.boolean().optional(), diff --git a/src/lib/submissionChangeDetection.ts b/src/lib/submissionChangeDetection.ts index d762e19a..20fb4406 100644 --- a/src/lib/submissionChangeDetection.ts +++ b/src/lib/submissionChangeDetection.ts @@ -21,9 +21,9 @@ export interface FieldChange { changeType: 'added' | 'removed' | 'modified'; metadata?: { isCreatingNewLocation?: boolean; - precision?: 'day' | 'month' | 'year'; - oldPrecision?: 'day' | 'month' | 'year'; - newPrecision?: 'day' | 'month' | 'year'; + precision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'; + oldPrecision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'; + newPrecision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'; }; } @@ -803,7 +803,7 @@ function formatEntityType(entityType: string): string { /** * Format field value for display */ -export function formatFieldValue(value: any, precision?: 'day' | 'month' | 'year'): string { +export function formatFieldValue(value: any, precision?: 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'): string { if (value === null || value === undefined) return 'None'; if (typeof value === 'boolean') return value ? 'Yes' : 'No'; @@ -817,9 +817,15 @@ export function formatFieldValue(value: any, precision?: 'day' | 'month' | 'year return date.getFullYear().toString(); } else if (precision === 'month') { return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' }); + } else if (precision === 'decade') { + return `${Math.floor(date.getFullYear() / 10) * 10}s`; + } else if (precision === 'century') { + return `${Math.ceil(date.getFullYear() / 100)}th century`; + } else if (precision === 'approximate') { + return `circa ${date.getFullYear()}`; } - // Default: full date + // Default: full date (exact) return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } catch { return String(value); diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index 42e1ca6c..b6fe943d 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -659,9 +659,9 @@ export default function ParkDetail() { park_type: park?.park_type, status: park?.status, opening_date: park?.opening_date ?? undefined, - opening_date_precision: (park?.opening_date_precision as 'day' | 'month' | 'year') ?? undefined, + opening_date_precision: (park?.opening_date_precision as 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate') ?? undefined, closing_date: park?.closing_date ?? undefined, - closing_date_precision: (park?.closing_date_precision as 'day' | 'month' | 'year') ?? undefined, + closing_date_precision: (park?.closing_date_precision as 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate') ?? undefined, location_id: park?.location?.id, location: park?.location ? { name: park.location.name || '', diff --git a/src/types/timeline.ts b/src/types/timeline.ts index 94bad60d..b73692c9 100644 --- a/src/types/timeline.ts +++ b/src/types/timeline.ts @@ -22,7 +22,7 @@ export type TimelineEventType = export type EntityType = 'park' | 'ride' | 'company' | 'ride_model'; -export type DatePrecision = 'day' | 'month' | 'year'; +export type DatePrecision = 'exact' | 'month' | 'year' | 'decade' | 'century' | 'approximate'; /** * Timeline event stored in database after approval diff --git a/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql b/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql new file mode 100644 index 00000000..ea580fae --- /dev/null +++ b/supabase/migrations/20251111220222_59da5009-54b6-4fef-acb2-d75943214f1a.sql @@ -0,0 +1,99 @@ +-- Phase 3: Migrate 'day' precision to 'exact' across all tables +-- This fixes the check constraint violations and aligns with the new precision system + +-- Parks +UPDATE parks +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE parks +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Park Submissions +UPDATE park_submissions +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE park_submissions +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Park Versions +UPDATE park_versions +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE park_versions +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Rides +UPDATE rides +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE rides +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Ride Submissions +UPDATE ride_submissions +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE ride_submissions +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Ride Versions +UPDATE ride_versions +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE ride_versions +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Companies +UPDATE companies +SET founded_date_precision = 'exact' +WHERE founded_date_precision = 'day'; + +-- Company Submissions +UPDATE company_submissions +SET founded_date_precision = 'exact' +WHERE founded_date_precision = 'day'; + +-- Company Versions +UPDATE company_versions +SET founded_date_precision = 'exact' +WHERE founded_date_precision = 'day'; + +-- Entity Timeline Events +UPDATE entity_timeline_events +SET event_date_precision = 'exact' +WHERE event_date_precision = 'day'; + +-- Timeline Event Submissions +UPDATE timeline_event_submissions +SET event_date_precision = 'exact' +WHERE event_date_precision = 'day'; + +-- Historical Parks +UPDATE historical_parks +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE historical_parks +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; + +-- Historical Rides +UPDATE historical_rides +SET opening_date_precision = 'exact' +WHERE opening_date_precision = 'day'; + +UPDATE historical_rides +SET closing_date_precision = 'exact' +WHERE closing_date_precision = 'day'; \ No newline at end of file