← back to Watches

src/components/EnhancedDashboard.jsx

395 lines

import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
import { Doughnut, Bar, Scatter } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  ArcElement,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  Title,
  Tooltip,
  Legend
} from 'chart.js';
import { FiWatch, FiTrendingUp, FiDollarSign, FiDatabase, FiAward } from 'react-icons/fi';
import { StatCardSkeleton } from './SkeletonLoader';

ChartJS.register(
  ArcElement,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  Title,
  Tooltip,
  Legend
);

/**
 * Enhanced dashboard with advanced data visualizations and interactive charts
 */
function EnhancedDashboard({ statistics, watches, onWatchSelect, loading }) {
  const chartData = useMemo(() => {
    if (!watches || watches.length === 0) return null;

    // Series distribution
    const seriesMap = {};
    watches.forEach(watch => {
      seriesMap[watch.series] = (seriesMap[watch.series] || 0) + 1;
    });

    const seriesDistribution = {
      labels: Object.keys(seriesMap),
      datasets: [{
        data: Object.values(seriesMap),
        backgroundColor: [
          '#C8102E',
          '#FFD700',
          '#1A1A2E',
          '#C0C0C0',
          '#FF6384',
          '#36A2EB',
          '#FFCE56',
          '#4BC0C0'
        ],
        borderWidth: 2,
        borderColor: '#fff'
      }]
    };

    // Price distribution by year
    const yearPriceMap = {};
    watches.forEach(watch => {
      const year = watch.yearIntroduced;
      const currentPrice = watch.priceHistory[watch.priceHistory.length - 1]?.price || 0;
      if (!yearPriceMap[year]) yearPriceMap[year] = [];
      yearPriceMap[year].push(currentPrice);
    });

    const yearLabels = Object.keys(yearPriceMap).sort();
    const avgPrices = yearLabels.map(year => {
      const prices = yearPriceMap[year];
      return prices.reduce((sum, p) => sum + p, 0) / prices.length;
    });

    const priceByYear = {
      labels: yearLabels,
      datasets: [{
        label: 'Average Current Price',
        data: avgPrices,
        backgroundColor: '#C8102E',
        borderColor: '#C8102E',
        borderWidth: 2
      }]
    };

    // Appreciation vs Price scatter
    const scatterData = watches.map(watch => {
      const first = watch.priceHistory[0].price;
      const last = watch.priceHistory[watch.priceHistory.length - 1].price;
      const appreciation = ((last - first) / first) * 100;
      return {
        x: last,
        y: appreciation,
        label: watch.model
      };
    });

    const appreciationScatter = {
      datasets: [{
        label: 'Watches',
        data: scatterData,
        backgroundColor: '#C8102E',
        borderColor: '#1A1A2E',
        borderWidth: 2,
        pointRadius: 6,
        pointHoverRadius: 8
      }]
    };

    return { seriesDistribution, priceByYear, appreciationScatter };
  }, [watches]);

  if (loading || !statistics) {
    return (
      <div className="space-y-6">
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
          {[1, 2, 3, 4].map(i => <StatCardSkeleton key={i} />)}
        </div>
      </div>
    );
  }

  const totalDataPoints = watches.reduce((sum, w) => sum + w.priceHistory.length, 0);
  const avgCurrentPrice = watches.reduce((sum, w) => {
    return sum + (w.priceHistory[w.priceHistory.length - 1]?.price || 0);
  }, 0) / watches.length;

  return (
    <div className="space-y-6">
      {/* Statistics Cards */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        <StatCard
          title="Total Watches"
          value={statistics.totalWatches}
          icon={FiWatch}
          color="from-blue-500 to-blue-600"
          delay={0}
        />
        <StatCard
          title="Watch Series"
          value={statistics.totalSeries}
          icon={FiDatabase}
          color="from-green-500 to-green-600"
          delay={0.1}
        />
        <StatCard
          title="Avg Appreciation"
          value={`${statistics.averageAppreciation}%`}
          icon={FiTrendingUp}
          color="from-purple-500 to-purple-600"
          delay={0.2}
        />
        <StatCard
          title="Avg Price"
          value={`$${Math.round(avgCurrentPrice).toLocaleString()}`}
          icon={FiDollarSign}
          color="from-orange-500 to-orange-600"
          delay={0.3}
        />
      </div>

      {/* Charts Grid */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Series Distribution */}
        <ChartCard title="Collection Distribution" delay={0.4}>
          <Doughnut
            data={chartData.seriesDistribution}
            options={{
              responsive: true,
              maintainAspectRatio: false,
              plugins: {
                legend: {
                  position: 'right',
                  labels: {
                    padding: 15,
                    font: { size: 11 }
                  }
                },
                tooltip: {
                  callbacks: {
                    label: (context) => {
                      const label = context.label || '';
                      const value = context.parsed || 0;
                      const total = context.dataset.data.reduce((a, b) => a + b, 0);
                      const percentage = ((value / total) * 100).toFixed(1);
                      return `${label}: ${value} (${percentage}%)`;
                    }
                  }
                }
              }
            }}
          />
        </ChartCard>

        {/* Price by Year */}
        <ChartCard title="Average Price by Year" delay={0.5}>
          <Bar
            data={chartData.priceByYear}
            options={{
              responsive: true,
              maintainAspectRatio: false,
              plugins: {
                legend: { display: false },
                tooltip: {
                  callbacks: {
                    label: (context) => `$${context.parsed.y.toLocaleString()}`
                  }
                }
              },
              scales: {
                y: {
                  ticks: {
                    callback: (value) => `$${(value / 1000).toFixed(0)}K`
                  }
                }
              }
            }}
          />
        </ChartCard>
      </div>

      {/* Appreciation vs Price Scatter */}
      <ChartCard title="Price vs Appreciation Analysis" delay={0.6}>
        <Scatter
          data={chartData.appreciationScatter}
          options={{
            responsive: true,
            maintainAspectRatio: false,
            plugins: {
              legend: { display: false },
              tooltip: {
                callbacks: {
                  label: (context) => {
                    const point = context.raw;
                    return [
                      `${point.label}`,
                      `Price: $${point.x.toLocaleString()}`,
                      `Appreciation: ${point.y.toFixed(2)}%`
                    ];
                  }
                }
              }
            },
            scales: {
              x: {
                title: { display: true, text: 'Current Price ($)' },
                ticks: {
                  callback: (value) => `$${(value / 1000).toFixed(0)}K`
                }
              },
              y: {
                title: { display: true, text: 'Appreciation (%)' },
                ticks: {
                  callback: (value) => `${value}%`
                }
              }
            }
          }}
        />
      </ChartCard>

      {/* Top Performers */}
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ delay: 0.7 }}
        className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
      >
        <div className="flex items-center space-x-3 mb-6">
          <FiAward className="text-3xl text-omega-red" />
          <h2 className="text-2xl font-bold text-gray-800 dark:text-white">
            Top Performing Watches
          </h2>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          {statistics.topPerformers.map((watch, index) => (
            <motion.div
              key={watch.id}
              initial={{ opacity: 0, x: -20 }}
              animate={{ opacity: 1, x: 0 }}
              transition={{ delay: 0.7 + index * 0.1 }}
              className="flex items-center justify-between p-4 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-600 rounded-lg hover:shadow-md transition-all cursor-pointer group"
              onClick={() => {
                const fullWatch = watches.find(w => w.id === watch.id);
                if (fullWatch) onWatchSelect(fullWatch);
              }}
            >
              <div className="flex items-center space-x-4">
                <div className="text-3xl font-bold text-omega-red w-12 text-center">
                  #{index + 1}
                </div>
                <div>
                  <h3 className="font-semibold text-gray-800 dark:text-white group-hover:text-omega-red transition-colors">
                    {watch.model}
                  </h3>
                  <p className="text-sm text-gray-500 dark:text-gray-400">{watch.series}</p>
                </div>
              </div>
              <div className="text-right">
                <p className="text-2xl font-bold text-green-600">
                  +{watch.appreciation}%
                </p>
                <p className="text-sm text-gray-500 dark:text-gray-400">
                  ${watch.currentPrice.toLocaleString()}
                </p>
              </div>
            </motion.div>
          ))}
        </div>
      </motion.div>

      {/* Collection Summary */}
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ delay: 0.8 }}
        className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
      >
        <h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-6">
          Collection Overview
        </h2>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
          {getSeriesBreakdown(watches).map((series, index) => (
            <motion.div
              key={series.name}
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              transition={{ delay: 0.8 + index * 0.05 }}
              className="p-4 bg-gradient-to-br from-omega-navy to-omega-red text-white rounded-lg hover:shadow-lg transition-shadow"
            >
              <h3 className="text-sm font-bold mb-2 opacity-90">{series.name}</h3>
              <p className="text-3xl font-bold">{series.count}</p>
              <p className="text-xs opacity-80">models tracked</p>
            </motion.div>
          ))}
        </div>
      </motion.div>
    </div>
  );
}

function StatCard({ title, value, icon: Icon, color, delay }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay }}
      whileHover={{ scale: 1.02, y: -5 }}
      className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 hover:shadow-xl transition-all"
    >
      <div className="flex items-center justify-between">
        <div>
          <p className="text-gray-500 dark:text-gray-400 text-sm font-medium">{title}</p>
          <p className="text-3xl font-bold text-gray-800 dark:text-white mt-2">{value}</p>
        </div>
        <div className={`bg-gradient-to-br ${color} w-16 h-16 rounded-full flex items-center justify-center shadow-lg`}>
          <Icon className="text-white text-2xl" />
        </div>
      </div>
    </motion.div>
  );
}

function ChartCard({ title, children, delay }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ delay }}
      className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6"
    >
      <h3 className="text-lg font-bold text-gray-800 dark:text-white mb-4">{title}</h3>
      <div className="h-64 md:h-80">
        {children}
      </div>
    </motion.div>
  );
}

function getSeriesBreakdown(watches) {
  const seriesMap = {};
  watches.forEach(watch => {
    seriesMap[watch.series] = (seriesMap[watch.series] || 0) + 1;
  });

  return Object.entries(seriesMap)
    .map(([name, count]) => ({ name, count }))
    .sort((a, b) => b.count - a.count);
}

export default EnhancedDashboard;