CROP
ProjectsAdmin Panel

DIS Pagination UI Improvement Plan

This plan addresses UI/UX improvements for the two-phase paginated loading system implemented in the DIS Analytics Dashboard. The goal is to provide clear...

DIS Pagination UI Improvement Plan

Overview

This plan addresses UI/UX improvements for the two-phase paginated loading system implemented in the DIS Analytics Dashboard. The goal is to provide clear visual feedback during both loading phases and improve overall user experience.

Current State Analysis

New Hook States (from use-dis-progressive.ts)

// Overall loading status
type LoadingStatus = "idle" | "loading-vendors" | "loading-data" | "loading-background" | "complete" | "error";

// Per-vendor status
status: "pending" | "loading" | "loaded" | "loading-more" | "error";
pageProgress?: number; // 0-100 for multi-page vendors

Problems Identified

  1. No visual distinction for "loading-more" - Background page loading looks like "pending"
  2. No visual distinction for "loading-background" - Phase 2 looks identical to Phase 1
  3. Progress bar misleading - Shows vendor count, not actual work done
  4. No page-level progress - User can't see how far along each vendor is
  5. Missing tooltips - No detailed info on hover

Available shadcn/ui Components

  • Badge - variants: default, secondary, destructive, outline
  • Progress - single value progress bar
  • Tooltip / TooltipTrigger / TooltipContent - hover info
  • Card - container styling
  • Skeleton - loading placeholders

Phase 1: Badge Styling for New States

Goal: Visually distinguish all vendor states including the new "loading-more".

1.1 Define Color Scheme

StateColorIconDescription
pendingGray (outline)NoneWaiting to load
loadingBlueSpinnerFirst page loading (Phase 1)
loading-moreIndigo/PurpleSpinner + %Background pages loading (Phase 2)
loadedGreenCheckmarkAll pages complete
errorRedRetry iconFailed, click to retry

1.2 Badge Component Updates

// New badge styling in ProgressiveLoadingOverlay
const getBadgeStyle = (status: VendorLoadState['status']) => {
  switch (status) {
    case 'loading':
      return 'bg-blue-100 text-blue-700 border-blue-200';
    case 'loading-more':
      return 'bg-indigo-100 text-indigo-700 border-indigo-200'; // NEW
    case 'loaded':
      return 'bg-emerald-100 text-emerald-700 border-emerald-200';
    case 'error':
      return 'bg-red-100 text-red-700 border-red-200 cursor-pointer';
    default: // pending
      return 'border-muted-foreground/30';
  }
};

1.3 Add Page Progress to Badge

// Show page progress inside badge for loading-more state
<Badge className={getBadgeStyle(v.status)}>
  {v.status === 'loading' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
  {v.status === 'loading-more' && (
    <>
      <Loader2 className="h-3 w-3 mr-1 animate-spin" />
      <span className="text-[10px] font-mono mr-1">{v.pageProgress}%</span>
    </>
  )}
  {v.status === 'loaded' && <CheckCircle2 className="h-3 w-3 mr-1" />}
  {v.status === 'error' && <RotateCcw className="h-3 w-3 mr-1" />}
  {v.name}
</Badge>

Files to Modify

  • app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx

Estimated Changes

  • ~30 lines modified

Phase 2: Tooltip for Detailed Progress

Goal: Show detailed loading info on badge hover.

2.1 Add Tooltip Component

import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

// Wrap each badge in tooltip
<Tooltip>
  <TooltipTrigger asChild>
    <Badge className={getBadgeStyle(v.status)} onClick={...}>
      {/* badge content */}
    </Badge>
  </TooltipTrigger>
  <TooltipContent>
    <div className="text-xs space-y-1">
      <div className="font-medium">{v.name} ({v.code})</div>
      <div className="text-muted-foreground">{v.partsCount.toLocaleString()} parts total</div>
      {v.status === 'loading-more' && v.pageProgress !== undefined && (
        <div>
          Loading: {v.pageProgress}%
          <Progress value={v.pageProgress} className="h-1 mt-1" />
        </div>
      )}
      {v.status === 'error' && (
        <div className="text-red-400">Click to retry</div>
      )}
    </div>
  </TooltipContent>
</Tooltip>

2.2 VendorBadge Component Extraction

Extract badge logic into reusable component:

// New file: dis-vendor-badge.tsx
interface VendorBadgeProps {
  vendor: VendorLoadState;
  onRetry?: (code: string) => void;
}

export function VendorBadge({ vendor, onRetry }: VendorBadgeProps) {
  const { code, name, partsCount, status, pageProgress, error } = vendor;

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Badge
          className={getBadgeStyle(status)}
          onClick={status === 'error' ? () => onRetry?.(code) : undefined}
        >
          <VendorBadgeIcon status={status} />
          {status === 'loading-more' && pageProgress !== undefined && (
            <span className="text-[10px] font-mono">{pageProgress}%</span>
          )}
          {name}
        </Badge>
      </TooltipTrigger>
      <TooltipContent side="bottom" className="max-w-[200px]">
        <VendorTooltipContent vendor={vendor} />
      </TooltipContent>
    </Tooltip>
  );
}

Files to Create

  • app/(dashboard)/dashboard/dis/_components/dis-vendor-badge.tsx

Files to Modify

  • app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx

Estimated Changes

  • ~80 lines new file
  • ~20 lines modified

Phase 3: Two-Phase Loading Overlay

Goal: Different UI for Phase 1 (blocking) vs Phase 2 (background).

3.1 Phase 1 Overlay (Current - Enhanced)

Full-width card with progress, shows during loading-vendors and loading-data:

// Existing but enhanced
<Card className="border-blue-200 bg-blue-50/50">
  <CardHeader>
    <div className="flex items-center gap-3">
      <Loader2 className="h-5 w-5 animate-spin text-blue-500" />
      <div>
        <CardTitle>Loading DIS Analytics</CardTitle>
        <CardDescription>
          Phase 1: Loading first page for each vendor...
        </CardDescription>
      </div>
    </div>
  </CardHeader>
  <CardContent>
    <Progress value={progress} />
    <div className="flex flex-wrap gap-2 mt-3">
      {vendors.map(v => <VendorBadge key={v.code} vendor={v} />)}
    </div>
  </CardContent>
</Card>

3.2 Phase 2 Overlay (New - Minimal)

Compact notification bar during loading-background:

// New minimal overlay for background loading
{status === 'loading-background' && (
  <div className="flex items-center justify-between p-3 rounded-lg border border-indigo-200 bg-indigo-50/50">
    <div className="flex items-center gap-2">
      <Loader2 className="h-4 w-4 animate-spin text-indigo-500" />
      <span className="text-sm text-indigo-700">
        Loading remaining data in background...
      </span>
      <div className="flex gap-1">
        {vendors.filter(v => v.status === 'loading-more').map(v => (
          <Badge key={v.code} variant="outline" className="text-[10px] py-0">
            {v.code} {v.pageProgress}%
          </Badge>
        ))}
      </div>
    </div>
    <Button variant="ghost" size="sm" onClick={cancel}>
      <XCircle className="h-4 w-4" />
    </Button>
  </div>
)}

3.3 Conditional Rendering Logic

// In main component
return (
  <div className="space-y-6">
    {/* Phase 1: Full overlay when no data */}
    {(status === 'loading-vendors' || status === 'loading-data') && !hasData && (
      <Phase1LoadingOverlay {...props} />
    )}

    {/* Phase 1: Compact overlay when refreshing with data */}
    {(status === 'loading-vendors' || status === 'loading-data') && hasData && (
      <Phase1RefreshOverlay {...props} />
    )}

    {/* Phase 2: Minimal background indicator */}
    {status === 'loading-background' && (
      <BackgroundLoadingBar {...props} />
    )}

    {/* Main content */}
    {hasData && <DashboardContent data={data} />}
  </div>
);

Files to Modify

  • app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx

Estimated Changes

  • ~100 lines modified/added

Phase 4: Accurate Progress Calculation

Goal: Show actual work completed, not just vendor count.

4.1 Calculate True Progress

// In hook: calculate weighted progress based on parts
const calculateTrueProgress = (vendors: VendorLoadState[], accumulators: Map<string, VendorAccumulator>) => {
  if (vendors.length === 0) return 0;

  const totalParts = vendors.reduce((sum, v) => sum + v.partsCount, 0);
  let loadedParts = 0;

  for (const v of vendors) {
    if (v.status === 'loaded') {
      loadedParts += v.partsCount;
    } else if (v.status === 'loading-more' && v.pageProgress !== undefined) {
      loadedParts += Math.round(v.partsCount * v.pageProgress / 100);
    }
  }

  return Math.round((loadedParts / totalParts) * 100);
};

4.2 Expose from Hook

// Add to hook return
return {
  // ... existing
  trueProgress, // weighted by parts count
  progress,     // by vendor count (for compatibility)
};

Files to Modify

  • lib/hooks/use-dis-progressive.ts
  • app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx

Estimated Changes

  • ~30 lines in hook
  • ~10 lines in dashboard

Phase 5: Performance Fixes (Hook)

Goal: Fix critical issues from code review.

5.1 Concurrent Page Loading in Phase 2

// Current: Sequential
for (const { code, totalPages } of vendorsWithMorePages) {
  for (let page = 2; page <= totalPages; page++) {
    await fetchVendorPage(code, page); // One at a time!
  }
}

// Better: Parallel batches
const allPages = vendorsWithMorePages.flatMap(({ code, totalPages }) =>
  Array.from({ length: totalPages - 1 }, (_, i) => ({ code, page: i + 2 }))
);

for (let i = 0; i < allPages.length && !abortRef.current; i += CONCURRENCY) {
  const batch = allPages.slice(i, i + CONCURRENCY);
  const results = await Promise.all(
    batch.map(({ code, page }) => fetchVendorPage(code, page))
  );
  // Process results...
}

5.2 Add AbortController

const abortControllerRef = useRef<AbortController | null>(null);

const fetchVendorPage = useCallback(async (vendorCode: string, page = 1) => {
  const controller = abortControllerRef.current;
  const signal = controller?.signal;

  const res = await fetch(url, {
    signal,
    // Add timeout via AbortSignal.timeout when supported
  });
  // ...
}, [environment]);

const cancel = useCallback(() => {
  abortControllerRef.current?.abort();
  abortControllerRef.current = new AbortController();
  setStatus(cachedData ? 'complete' : 'idle');
}, [cachedData]);

const startLoading = useCallback(async () => {
  abortControllerRef.current = new AbortController();
  // ...
}, [...]);

5.3 Batch Cache Updates

// Current: Update cache on every page
for (let page = 2; page <= totalPages; page++) {
  await fetchVendorPage(code, page);
  queryClient.setQueryData(...); // Every page!
}

// Better: Update per vendor or per batch
for (const { code, totalPages } of vendorsWithMorePages) {
  for (let page = 2; page <= totalPages; page++) {
    await fetchVendorPage(code, page);
  }
  // Update cache once per vendor
  updateCacheForVendor(code);
}

Files to Modify

  • lib/hooks/use-dis-progressive.ts

Estimated Changes

  • ~100 lines modified

Implementation Order

Phase 1 (Badge Styling)        [2 hours]  - Quick visual win

Phase 2 (Tooltips)             [2 hours]  - Better UX feedback

Phase 3 (Two-Phase Overlay)    [3 hours]  - Clear phase distinction

Phase 4 (True Progress)        [1 hour]   - Accurate feedback

Phase 5 (Performance)          [4 hours]  - Critical fixes

Ticket Breakdown

Ticket 1: DIS-UI-001 Badge & Tooltip Improvements

  • Phase 1 + Phase 2
  • Files: dis-analytics-dashboard.tsx, dis-vendor-badge.tsx (new)
  • Priority: High

Ticket 2: DIS-UI-002 Two-Phase Loading Overlay

  • Phase 3
  • Files: dis-analytics-dashboard.tsx
  • Priority: Medium

Ticket 3: DIS-UI-003 Accurate Progress Calculation

  • Phase 4
  • Files: use-dis-progressive.ts, dis-analytics-dashboard.tsx
  • Priority: Low

Ticket 4: DIS-PERF-001 Concurrent Page Loading

  • Phase 5.1
  • Files: use-dis-progressive.ts
  • Priority: High (Performance)

Ticket 5: DIS-ROBUST-001 AbortController & Cancellation

  • Phase 5.2 + 5.3
  • Files: use-dis-progressive.ts
  • Priority: High (Robustness)

Testing Checklist

  • Phase 1 loading shows blue badges with spinner
  • Phase 2 loading shows indigo badges with percentage
  • Completed vendors show green badges with checkmark
  • Error vendors show red badges, clickable for retry
  • Tooltip shows vendor details on hover
  • Background loading shows minimal notification bar
  • Cancel button works in both phases
  • Progress reflects actual parts loaded, not vendor count
  • Large vendors (BNS 1300+ parts) load first page in <5s
  • Cancellation actually stops in-flight requests

Mockups

Badge States

[pending]     [ ] Kuhn
[loading]     [🔄] Kuhn
[loading-more] [🔄 45%] Kuhn
[loaded]      [✓] Kuhn
[error]       [↻] Kuhn

Phase 2 Notification Bar

┌─────────────────────────────────────────────────────────────┐
│ 🔄 Loading remaining data...  [BNS 45%] [KUH 78%]     [✕]  │
└─────────────────────────────────────────────────────────────┘

On this page