mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-25 18:31:12 -05:00
Adds comprehensive data completeness dashboard UI and hooks: - Introduces data completeness types and hook (useDataCompleteness) to fetch and subscribe to updates - Builds dashboard components (summary, filters, table) and integrates into Admin Settings - Wireframes for real-time updates and filtering across parks, rides, companies, and ride models - Integrates into AdminSettings with a new Data Quality tab and route - Adds data types and scaffolding for analytics, including completeness analysis structure
147 lines
5.1 KiB
TypeScript
147 lines
5.1 KiB
TypeScript
/**
|
|
* Data Completeness Table Component
|
|
*
|
|
* Virtualized table displaying entity completeness data with sorting and actions
|
|
*/
|
|
|
|
import { useMemo } from 'react';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ExternalLink, AlertCircle } from 'lucide-react';
|
|
import { Link } from 'react-router-dom';
|
|
import type { EntityCompleteness, CompletenessFilters } from '@/types/data-completeness';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
|
|
interface CompletenessTableProps {
|
|
entities: EntityCompleteness[];
|
|
filters: CompletenessFilters;
|
|
}
|
|
|
|
export function CompletenessTable({ entities, filters }: CompletenessTableProps) {
|
|
// Filter and sort entities
|
|
const filteredEntities = useMemo(() => {
|
|
let filtered = entities;
|
|
|
|
// Apply search filter
|
|
if (filters.searchQuery) {
|
|
const query = filters.searchQuery.toLowerCase();
|
|
filtered = filtered.filter((entity) =>
|
|
entity.name.toLowerCase().includes(query)
|
|
);
|
|
}
|
|
|
|
// Sort by completeness score (ascending - most incomplete first)
|
|
return filtered.sort((a, b) => a.completeness_score - b.completeness_score);
|
|
}, [entities, filters]);
|
|
|
|
const getEntityUrl = (entity: EntityCompleteness) => {
|
|
switch (entity.entity_type) {
|
|
case 'park':
|
|
return `/parks/${entity.slug}`;
|
|
case 'ride':
|
|
return `/rides/${entity.slug}`;
|
|
case 'company':
|
|
return `/companies/${entity.slug}`;
|
|
case 'ride_model':
|
|
return `/ride-models/${entity.slug}`;
|
|
default:
|
|
return '#';
|
|
}
|
|
};
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 80) return 'text-green-600';
|
|
if (score >= 50) return 'text-yellow-600';
|
|
return 'text-destructive';
|
|
};
|
|
|
|
const getMissingFieldsCount = (entity: EntityCompleteness) => {
|
|
return (
|
|
entity.missing_fields.critical.length +
|
|
entity.missing_fields.important.length +
|
|
entity.missing_fields.valuable.length +
|
|
entity.missing_fields.supplementary.length
|
|
);
|
|
};
|
|
|
|
if (filteredEntities.length === 0) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<AlertCircle className="h-12 w-12 text-muted-foreground mb-4" />
|
|
<p className="text-lg font-medium">No entities found</p>
|
|
<p className="text-sm text-muted-foreground">Try adjusting your filters</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="border rounded-lg">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Entity</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Completeness</TableHead>
|
|
<TableHead>Missing Fields</TableHead>
|
|
<TableHead>Last Updated</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredEntities.map((entity) => (
|
|
<TableRow key={entity.id}>
|
|
<TableCell className="font-medium">{entity.name}</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">
|
|
{entity.entity_type.replace('_', ' ')}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className={`text-sm font-medium ${getScoreColor(entity.completeness_score)}`}>
|
|
{entity.completeness_score.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<Progress value={entity.completeness_score} className="h-2" />
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-wrap gap-1">
|
|
{entity.missing_fields.critical.length > 0 && (
|
|
<Badge variant="destructive" className="text-xs">
|
|
{entity.missing_fields.critical.length} Critical
|
|
</Badge>
|
|
)}
|
|
{entity.missing_fields.important.length > 0 && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
{entity.missing_fields.important.length} Important
|
|
</Badge>
|
|
)}
|
|
{getMissingFieldsCount(entity) === 0 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
Complete
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{formatDistanceToNow(new Date(entity.updated_at), { addSuffix: true })}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button variant="ghost" size="sm" asChild>
|
|
<Link to={getEntityUrl(entity)}>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Link>
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|