feat: Enable TypeScript strict mode

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 00:58:42 +00:00
parent 061c06be29
commit d126be2908
15 changed files with 308 additions and 68 deletions

View File

@@ -79,7 +79,7 @@ interface ParkFormProps {
onSubmit: (data: ParkFormData & { onSubmit: (data: ParkFormData & {
operator_id?: string; operator_id?: string;
property_owner_id?: string; property_owner_id?: string;
_compositeSubmission?: any; _compositeSubmission?: import('@/types/composite-submission').ParkCompositeSubmission;
}) => Promise<void>; }) => Promise<void>;
onCancel?: () => void; onCancel?: () => void;
initialData?: Partial<ParkFormData & { initialData?: Partial<ParkFormData & {
@@ -214,7 +214,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
} }
// Build composite submission if new entities were created // Build composite submission if new entities were created
const submissionContent: any = { const submissionContent: import('@/types/composite-submission').ParkCompositeSubmission = {
park: data, park: data,
}; };

View File

@@ -90,7 +90,11 @@ export function ConflictResolutionDialog({
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertDescription> <AlertDescription>
<p className="font-medium"> <p className="font-medium">
{item?.item_type.replace('_', ' ').toUpperCase()}: {item?.item_data.name} {item?.item_type.replace('_', ' ').toUpperCase()}: {
item && typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data
? String((item.item_data as Record<string, unknown>).name)
: 'Unnamed'
}
</p> </p>
<p className="text-sm mt-1">{conflict.message}</p> <p className="text-sm mt-1">{conflict.message}</p>
</AlertDescription> </AlertDescription>

View File

@@ -24,8 +24,10 @@ export function DependencyTreeView({ items }: DependencyTreeViewProps) {
}; };
const getItemLabel = (item: SubmissionItemWithDeps): string => { const getItemLabel = (item: SubmissionItemWithDeps): string => {
const data = item.item_data; const data = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
const name = data.name || 'Unnamed'; ? item.item_data as Record<string, unknown>
: {};
const name = 'name' in data && typeof data.name === 'string' ? data.name : 'Unnamed';
const type = item.item_type.replace('_', ' '); const type = item.item_type.replace('_', ' ');
return `${name} (${type})`; return `${name} (${type})`;
}; };

View File

@@ -104,7 +104,9 @@ export function DependencyVisualizer({ items, selectedIds }: DependencyVisualize
</CardHeader> </CardHeader>
<CardContent className={isMobile ? "p-4 pt-0" : ""}> <CardContent className={isMobile ? "p-4 pt-0" : ""}>
<p className={`font-medium ${isMobile ? 'text-sm' : 'text-sm'}`}> <p className={`font-medium ${isMobile ? 'text-sm' : 'text-sm'}`}>
{item.item_data.name || 'Unnamed'} {typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data
? String((item.item_data as Record<string, unknown>).name)
: 'Unnamed'}
</p> </p>
{item.dependents && item.dependents.length > 0 && ( {item.dependents && item.dependents.length > 0 && (
<p className={`text-muted-foreground mt-1 ${isMobile ? 'text-xs' : 'text-xs'}`}> <p className={`text-muted-foreground mt-1 ${isMobile ? 'text-xs' : 'text-xs'}`}>

View File

@@ -93,15 +93,21 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }:
}; };
const handlePhotoSubmit = async (caption: string, credit: string) => { const handlePhotoSubmit = async (caption: string, credit: string) => {
const itemData = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
const photos = 'photos' in itemData && Array.isArray(itemData.photos)
? itemData.photos
: [];
const photoData = { const photoData = {
...item.item_data, ...itemData,
photos: Array.isArray(item.item_data.photos) photos: photos.map((photo: unknown) => ({
? item.item_data.photos.map((photo: unknown) => ({ ...(typeof photo === 'object' && photo !== null ? photo as Record<string, unknown> : {}),
...(typeof photo === 'object' && photo !== null ? photo : {}), caption,
caption, credit,
credit, })),
}))
: [],
}; };
await handleSubmit(photoData); await handleSubmit(photoData);
}; };

View File

@@ -127,7 +127,7 @@ export function ItemReviewCard({ item, onEdit, onStatusChange, submissionId }: I
<ValidationSummary <ValidationSummary
item={{ item={{
item_type: item.item_type, item_type: item.item_type,
item_data: item.item_data, item_data: item.item_data as Record<string, unknown>,
id: item.id, id: item.id,
}} }}
onValidationChange={handleValidationChange} onValidationChange={handleValidationChange}

View File

@@ -87,9 +87,9 @@ export function ItemSelectorDialog({
<span className="font-medium capitalize"> <span className="font-medium capitalize">
{item.item_type.replace('_', ' ')} {item.item_type.replace('_', ' ')}
</span> </span>
{item.item_data.name && ( {typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data && (
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{String(item.item_data.name)} {String((item.item_data as Record<string, unknown>).name)}
</span> </span>
)} )}
{item.dependencies && item.dependencies.length > 0 && ( {item.dependencies && item.dependencies.length > 0 && (

View File

@@ -145,7 +145,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
}; };
// Wrapped delete with confirmation // Wrapped delete with confirmation
const handleDeleteSubmission = useCallback((item: any) => { const handleDeleteSubmission = useCallback((item: { id: string; submission_type?: string }) => {
setConfirmDialog({ setConfirmDialog({
open: true, open: true,
title: 'Delete Submission', title: 'Delete Submission',
@@ -198,7 +198,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
enabled: true, enabled: true,
}); });
const handleOpenPhotos = (photos: any[], index: number) => { const handleOpenPhotos = (photos: PhotoItem[], index: number) => {
setSelectedPhotos(photos); setSelectedPhotos(photos);
setSelectedPhotoIndex(index); setSelectedPhotoIndex(index);
setPhotoModalOpen(true); setPhotoModalOpen(true);

View File

@@ -605,9 +605,12 @@ export function SubmissionReviewManager({
open={showValidationBlockerDialog} open={showValidationBlockerDialog}
onClose={() => setShowValidationBlockerDialog(false)} onClose={() => setShowValidationBlockerDialog(false)}
blockingErrors={Array.from(validationResults.values()).flatMap(r => r.blockingErrors)} blockingErrors={Array.from(validationResults.values()).flatMap(r => r.blockingErrors)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => {
i.item_data?.name || i.item_type.replace('_', ' ') const name = typeof i.item_data === 'object' && i.item_data !== null && !Array.isArray(i.item_data) && 'name' in i.item_data
)} ? String((i.item_data as Record<string, unknown>).name)
: i.item_type.replace('_', ' ');
return name;
})}
/> />
<WarningConfirmDialog <WarningConfirmDialog
@@ -619,9 +622,12 @@ export function SubmissionReviewManager({
handleApprove(); handleApprove();
}} }}
warnings={Array.from(validationResults.values()).flatMap(r => r.warnings)} warnings={Array.from(validationResults.values()).flatMap(r => r.warnings)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => {
i.item_data?.name || i.item_type.replace('_', ' ') const name = typeof i.item_data === 'object' && i.item_data !== null && !Array.isArray(i.item_data) && 'name' in i.item_data
)} ? String((i.item_data as Record<string, unknown>).name)
: i.item_type.replace('_', ' ');
return name;
})}
/> />
<ConflictResolutionModal <ConflictResolutionModal

View File

@@ -91,14 +91,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & import('@/types/recharts').TooltipProps
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>( >(
( (
{ {
@@ -115,7 +108,7 @@ const ChartTooltipContent = React.forwardRef<
color, color,
nameKey, nameKey,
labelKey, labelKey,
}: any, },
ref, ref,
) => { ) => {
const { config } = useChart(); const { config } = useChart();
@@ -163,7 +156,11 @@ const ChartTooltipContent = React.forwardRef<
{payload.map((item, index) => { {payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`; const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color; const indicatorColor = color ||
(typeof item.payload === 'object' && item.payload !== null && 'fill' in item.payload
? (item.payload as Record<string, unknown>).fill as string
: undefined) ||
item.color;
return ( return (
<div <div
@@ -229,12 +226,7 @@ const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { import('@/types/recharts').LegendProps
hideIcon?: boolean;
nameKey?: string;
payload?: any[];
verticalAlign?: "top" | "bottom";
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => { >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart(); const { config } = useChart();
@@ -247,7 +239,7 @@ const ChartLegendContent = React.forwardRef<
ref={ref} ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)} className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
> >
{payload.map((item: any) => { {payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`; const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key);

View File

@@ -137,8 +137,8 @@ export function useEntityCache() {
} }
try { try {
let data: any[] | null = null; let data: unknown[] | null = null;
let error: any = null; let error: unknown = null;
// Use type-safe table queries // Use type-safe table queries
switch (type) { switch (type) {
@@ -172,18 +172,20 @@ export function useEntityCache() {
} }
if (error) { if (error) {
logger.error(`Error fetching ${type}:`, error); logger.error(`Error fetching ${type}:`, { error: getErrorMessage(error as Error) });
return []; return [];
} }
// Cache the fetched entities // Cache the fetched entities
if (data) { if (data) {
data.forEach((entity: any) => { (data as Array<Record<string, unknown>>).forEach((entity) => {
setCached(type, entity.id, entity as EntityTypeMap[T]); if (entity && typeof entity === 'object' && 'id' in entity && 'name' in entity) {
setCached(type, entity.id as string, entity as EntityTypeMap[T]);
}
}); });
} }
return data || []; return (data as EntityTypeMap[T][]) || [];
} catch (error: unknown) { } catch (error: unknown) {
logger.error(`Failed to bulk fetch ${type}:`, { error: getErrorMessage(error) }); logger.error(`Failed to bulk fetch ${type}:`, { error: getErrorMessage(error) });
return []; return [];
@@ -194,7 +196,7 @@ export function useEntityCache() {
* Fetch and cache related entities based on submission content * Fetch and cache related entities based on submission content
* Automatically determines which entities to fetch from submission data * Automatically determines which entities to fetch from submission data
*/ */
const fetchRelatedEntities = useCallback(async (submissions: any[]): Promise<void> => { const fetchRelatedEntities = useCallback(async (submissions: Array<{ content?: Record<string, string | number>; submission_type?: string }>): Promise<void> => {
const rideIds = new Set<string>(); const rideIds = new Set<string>();
const parkIds = new Set<string>(); const parkIds = new Set<string>();
const companyIds = new Set<string>(); const companyIds = new Set<string>();
@@ -203,20 +205,20 @@ export function useEntityCache() {
submissions.forEach(submission => { submissions.forEach(submission => {
const content = submission.content; const content = submission.content;
if (content && typeof content === 'object') { if (content && typeof content === 'object') {
if (content.ride_id) rideIds.add(content.ride_id); if (typeof content.ride_id === 'string') rideIds.add(content.ride_id);
if (content.park_id) parkIds.add(content.park_id); if (typeof content.park_id === 'string') parkIds.add(content.park_id);
if (content.company_id) companyIds.add(content.company_id); if (typeof content.company_id === 'string') companyIds.add(content.company_id);
if (content.entity_id) { if (typeof content.entity_id === 'string') {
if (submission.submission_type === 'ride') rideIds.add(content.entity_id); if (submission.submission_type === 'ride') rideIds.add(content.entity_id);
if (submission.submission_type === 'park') parkIds.add(content.entity_id); if (submission.submission_type === 'park') parkIds.add(content.entity_id);
if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type)) { if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type || '')) {
companyIds.add(content.entity_id); companyIds.add(content.entity_id);
} }
} }
if (content.manufacturer_id) companyIds.add(content.manufacturer_id); if (typeof content.manufacturer_id === 'string') companyIds.add(content.manufacturer_id);
if (content.designer_id) companyIds.add(content.designer_id); if (typeof content.designer_id === 'string') companyIds.add(content.designer_id);
if (content.operator_id) companyIds.add(content.operator_id); if (typeof content.operator_id === 'string') companyIds.add(content.operator_id);
if (content.property_owner_id) companyIds.add(content.property_owner_id); if (typeof content.property_owner_id === 'string') companyIds.add(content.property_owner_id);
} }
}); });

View File

@@ -4,14 +4,15 @@ import { logger } from './logger';
import { extractCloudflareImageId } from './cloudflareImageUtils'; import { extractCloudflareImageId } from './cloudflareImageUtils';
// Core submission item interface with dependencies // Core submission item interface with dependencies
// Type safety for item_data will be added in Phase 5 after fixing components // NOTE: item_data and original_data use `unknown` because they contain dynamic structures
// that vary by item_type. Use type guards from @/types/submission-item-data.ts to access safely.
export interface SubmissionItemWithDeps { export interface SubmissionItemWithDeps {
id: string; id: string;
submission_id: string; submission_id: string;
item_type: string; item_type: string;
item_data: any; // Complex nested structure - will be typed properly in Phase 5 item_data: unknown; // Dynamic structure - use type guards for safe access
original_data: any; // Complex nested structure - will be typed properly in Phase 5 original_data?: unknown; // Dynamic structure - use type guards for safe access
action_type?: 'create' | 'edit' | 'delete'; action_type?: 'create' | 'edit' | 'delete';
status: 'pending' | 'approved' | 'rejected' | 'flagged' | 'skipped'; // Matches ReviewStatus from statuses.ts status: 'pending' | 'approved' | 'rejected' | 'flagged' | 'skipped'; // Matches ReviewStatus from statuses.ts
depends_on: string | null; depends_on: string | null;
@@ -43,7 +44,10 @@ export interface ConflictCheckResult {
serverVersion?: { serverVersion?: {
last_modified_at: string; last_modified_at: string;
last_modified_by: string; last_modified_by: string;
modified_by_profile?: any; modified_by_profile?: {
username: string;
display_name?: string;
};
} | null; } | null;
} }
@@ -110,18 +114,28 @@ export async function detectDependencyConflicts(
// Suggest creating parent // Suggest creating parent
if (parent.status !== 'rejected') { if (parent.status !== 'rejected') {
const parentData = typeof parent.item_data === 'object' && parent.item_data !== null && !Array.isArray(parent.item_data)
? parent.item_data as Record<string, unknown>
: {};
const parentName = 'name' in parentData && typeof parentData.name === 'string' ? parentData.name : 'Unnamed';
suggestions.push({ suggestions.push({
action: 'create_parent', action: 'create_parent',
label: `Also approve ${parent.item_type}: ${parent.item_data.name}`, label: `Also approve ${parent.item_type}: ${parentName}`,
}); });
} }
// Suggest linking to existing entity // Suggest linking to existing entity
if (parent.item_type === 'park') { if (parent.item_type === 'park') {
const parentData = typeof parent.item_data === 'object' && parent.item_data !== null && !Array.isArray(parent.item_data)
? parent.item_data as Record<string, unknown>
: {};
const parentName = 'name' in parentData && typeof parentData.name === 'string' ? parentData.name : '';
const { data: parks } = await supabase const { data: parks } = await supabase
.from('parks') .from('parks')
.select('id, name') .select('id, name')
.ilike('name', `%${parent.item_data.name}%`) .ilike('name', `%${parentName}%`)
.limit(3); .limit(3);
parks?.forEach(park => { parks?.forEach(park => {
@@ -189,11 +203,15 @@ export async function approveSubmissionItems(
try { try {
// Determine if this is an edit by checking for entity_id in item_data // Determine if this is an edit by checking for entity_id in item_data
const itemData = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
isEdit = !!( isEdit = !!(
item.item_data.park_id || ('park_id' in itemData && itemData.park_id) ||
item.item_data.ride_id || ('ride_id' in itemData && itemData.ride_id) ||
item.item_data.company_id || ('company_id' in itemData && itemData.company_id) ||
item.item_data.ride_model_id ('ride_model_id' in itemData && itemData.ride_model_id)
); );
// Create the entity based on type with dependency resolution // Create the entity based on type with dependency resolution

View File

@@ -0,0 +1,40 @@
/**
* Type definitions for composite submissions
* Used when creating multiple related entities in a single submission
*/
import type { TempCompanyData } from './company';
/**
* Composite submission structure for park creation with related entities
*/
export interface ParkCompositeSubmission {
park: {
name: string;
slug: string;
description?: string;
park_type: string;
status: string;
opening_date?: string;
closing_date?: string;
location_id?: string;
website_url?: string;
phone?: string;
email?: string;
operator_id?: string | null;
property_owner_id?: string | null;
[key: string]: unknown;
};
new_operator?: TempCompanyData;
new_property_owner?: TempCompanyData;
}
/**
* Generic composite submission content type
* Supports any entity type with optional related entities
*/
export interface CompositeSubmissionContent {
[entityKey: string]: {
[key: string]: unknown;
} | TempCompanyData | undefined;
}

55
src/types/recharts.ts Normal file
View File

@@ -0,0 +1,55 @@
/**
* Type definitions for Recharts payloads
* Provides type-safe alternatives to `any` in chart components
*/
import type { ReactNode } from 'react';
/**
* Generic chart payload item structure
*/
export interface ChartPayloadItem<T = unknown> {
value?: number | string;
name?: string;
dataKey?: string;
color?: string;
fill?: string;
payload?: T;
[key: string]: unknown;
}
/**
* Tooltip content props from Recharts
*/
export interface TooltipProps<T = unknown> {
active?: boolean;
payload?: ChartPayloadItem<T>[];
label?: string | number;
labelFormatter?: (value: unknown, payload: ChartPayloadItem<T>[]) => ReactNode;
formatter?: (
value: number | string,
name: string,
item: ChartPayloadItem<T>,
index: number,
payload: unknown
) => ReactNode;
className?: string;
indicator?: 'line' | 'dot' | 'dashed';
hideLabel?: boolean;
hideIndicator?: boolean;
color?: string;
nameKey?: string;
labelKey?: string;
labelClassName?: string;
}
/**
* Legend content props from Recharts
*/
export interface LegendProps<T = unknown> {
payload?: ChartPayloadItem<T>[];
className?: string;
hideIcon?: boolean;
nameKey?: string;
verticalAlign?: 'top' | 'bottom';
}

View File

@@ -0,0 +1,113 @@
/**
* Type-safe helpers for accessing submission item data
* Provides type guards and type narrowing for Json fields
*/
import type { Json } from '@/integrations/supabase/types';
/**
* Base structure that all item_data objects should have
*/
export interface BaseItemData {
name?: string;
slug?: string;
description?: string;
[key: string]: Json;
}
/**
* Type guard to safely check if Json is an object with a name property
*/
export function hasName(data: Json): data is { name: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'name' in data && typeof (data as Record<string, Json>).name === 'string';
}
/**
* Type guard to check if Json is a photos array
*/
export function hasPhotos(data: Json): data is { photos: Array<Record<string, Json>> } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'photos' in data && Array.isArray((data as Record<string, Json>).photos);
}
/**
* Type guard for manufacturer data
*/
export function hasManufacturer(data: Json): data is { manufacturer_id: string; manufacturer_name: string } & Record<string, Json> {
return (
typeof data === 'object' &&
data !== null &&
!Array.isArray(data) &&
'manufacturer_id' in data &&
'manufacturer_name' in data
);
}
/**
* Type guard for park data
*/
export function hasParkId(data: Json): data is { park_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'park_id' in data;
}
/**
* Type guard for ride data
*/
export function hasRideId(data: Json): data is { ride_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'ride_id' in data;
}
/**
* Type guard for company data
*/
export function hasCompanyId(data: Json): data is { company_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'company_id' in data;
}
/**
* Type guard for ride model data
*/
export function hasRideModelId(data: Json): data is { ride_model_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'ride_model_id' in data;
}
/**
* Safely get name from item_data
*/
export function getItemName(data: Json): string {
if (hasName(data)) {
return data.name;
}
return 'Unnamed';
}
/**
* Safely get photos from item_data
*/
export function getItemPhotos(data: Json): Array<Record<string, Json>> {
if (hasPhotos(data)) {
return data.photos;
}
return [];
}
/**
* Convert Json to a record for form usage
* Only use when you need to pass data to form components
*/
export function jsonToRecord(data: Json): Record<string, Json> {
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
return data as Record<string, Json>;
}
return {};
}
/**
* Type-safe way to access nested Json properties
*/
export function getProperty<T = Json>(data: Json, key: string): T | undefined {
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
const obj = data as Record<string, Json>;
return obj[key] as T | undefined;
}
return undefined;
}