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').
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 22:05:29 +00:00
parent 9ee84b31ff
commit d0c613031e
15 changed files with 189 additions and 43 deletions

View File

@@ -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 || '',

View File

@@ -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(),

View File

@@ -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')

View File

@@ -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;

View File

@@ -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) + '...';

View File

@@ -211,7 +211,13 @@ function DateFieldDisplay({ change, compact }: { change: FieldChange; compact: b
{formatFieldName(change.field)}
{precision && (
<Badge variant="outline" className="text-xs ml-2">
{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'}
</Badge>
)}
</div>

View File

@@ -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({
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="day">Exact Day</SelectItem>
<SelectItem value="exact">Exact Day</SelectItem>
<SelectItem value="month">Month Only</SelectItem>
<SelectItem value="year">Year Only</SelectItem>
<SelectItem value="decade">Decade</SelectItem>
<SelectItem value="century">Century</SelectItem>
<SelectItem value="approximate">Approximate</SelectItem>
</SelectContent>
</Select>
<FormMessage />

View File

@@ -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;

View File

@@ -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({
<div className="flex gap-2">
<div className="flex-1">
{localPrecision === 'day' && (
{(localPrecision === 'exact') && (
<DatePicker
date={value}
onSelect={(date) => 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') && (
<Input
type="number"
value={yearValue}
@@ -166,9 +172,12 @@ export function FlexibleDateInput({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="day">Use Full Date</SelectItem>
<SelectItem value="month">Use Month/Year</SelectItem>
<SelectItem value="year">Use Year Only</SelectItem>
<SelectItem value="exact">Exact Day</SelectItem>
<SelectItem value="month">Month & Year</SelectItem>
<SelectItem value="year">Year Only</SelectItem>
<SelectItem value="decade">Decade</SelectItem>
<SelectItem value="century">Century</SelectItem>
<SelectItem value="approximate">Approximate</SelectItem>
</SelectContent>
</Select>
</div>

View File

@@ -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')}`;
}

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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 || '',

View File

@@ -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

View File

@@ -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';