All files / src/features/flyer FlyerList.tsx

100% Statements 37/37
97.61% Branches 41/42
100% Functions 6/6
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169                                    1x           22x   22x 4x 4x         1x   3x 3x 1x   2x       22x               65x 65x       65x 65x 65x   65x 50x 46x 46x 4x 1x 1x   3x 3x               65x       65x               65x       65x 65x 65x       65x 15x         65x       1x                                             1x                                 4x                                      
// src/features/flyer/FlyerList.tsx
import React from 'react';
import toast from 'react-hot-toast';
import type { Flyer, UserProfile } from '../../types';
import { DocumentTextIcon } from '../../components/icons/DocumentTextIcon';
import { parseISO, format, isValid } from 'date-fns';
import { MapPinIcon, Trash2Icon } from 'lucide-react';
import { logger } from '../../services/logger.client';
import * as apiClient from '../../services/apiClient';
import { calculateDaysBetween, formatDateRange, getCurrentDateISOString } from '../../utils/dateUtils';
 
interface FlyerListProps {
  flyers: Flyer[];
  onFlyerSelect: (flyer: Flyer) => void;
  selectedFlyerId: number | null;
  profile: UserProfile | null;
}
 
export const FlyerList: React.FC<FlyerListProps> = ({
  flyers,
  onFlyerSelect,
  selectedFlyerId,
  profile,
}) => {
  const isAdmin = profile?.role === 'admin';
 
  const handleCleanupClick = async (e: React.MouseEvent, flyerId: number) => {
    e.stopPropagation(); // Prevent the row's onClick from firing
    if (
      !window.confirm(
        `Are you sure you want to clean up the files for flyer ID ${flyerId}? This action cannot be undone.`,
      )
    ) {
      return;
    }
    try {
      await apiClient.cleanupFlyerFiles(flyerId);
      toast.success(`Cleanup job for flyer ID ${flyerId} has been enqueued.`);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : 'Failed to enqueue cleanup job.');
    }
  };
 
  return (
    <div className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
      <h3 className="text-lg font-bold text-gray-800 dark:text-white p-4 border-b border-gray-200 dark:border-gray-700">
        Processed Flyers
      </h3>
      {flyers.length > 0 ? (
        <ul className="divide-y divide-gray-200 dark:divide-gray-700 max-h-96 overflow-y-auto">
          {flyers.map((flyer) => {
            const dateRange = formatDateRange(flyer.valid_from, flyer.valid_to);
            const verboseDateRange = formatDateRange(flyer.valid_from, flyer.valid_to, {
              verbose: true,
            });
 
            const daysLeft = calculateDaysBetween(getCurrentDateISOString(), flyer.valid_to);
            let daysLeftText = '';
            let daysLeftColor = '';
 
            if (daysLeft !== null) {
              if (daysLeft < 0) {
                daysLeftText = 'Expired';
                daysLeftColor = 'text-red-500 dark:text-red-400';
              } else if (daysLeft === 0) {
                daysLeftText = 'Expires today';
                daysLeftColor = 'text-orange-500 dark:text-orange-400';
              } else {
                daysLeftText = `Expires in ${daysLeft} day${daysLeft === 1 ? '' : 's'}`;
                daysLeftColor =
                  daysLeft <= 3
                    ? 'text-orange-500 dark:text-orange-400'
                    : 'text-green-600 dark:text-green-400';
              }
            }
 
            // Build a more detailed tooltip string
            const processedDate = isValid(parseISO(flyer.created_at))
              ? format(parseISO(flyer.created_at), "MMMM d, yyyy 'at' h:mm:ss a")
              : 'N/A';
 
            const tooltipLines = [
              `File: ${flyer.file_name}`,
              `Store: ${flyer.store?.name || 'Unknown'}`,
              `Address: ${flyer.store_address || 'N/A'}`,
              `Items: ${flyer.item_count}`,
              verboseDateRange || 'Validity: N/A',
              `Processed: ${processedDate}`,
            ];
            const tooltipText = tooltipLines.join('\n');
 
            // --- DEBUG LOGGING for icon display logic ---
            // Log the flyer object and the specific icon_url value before the conditional check.
            logger.debug(`[FlyerList] Checking icon for flyer ID ${flyer.flyer_id}.`, { flyer });
            const hasIconUrl = !!flyer.icon_url;
            logger.debug(
              `[FlyerList] Flyer ID ${flyer.flyer_id}: hasIconUrl is ${hasIconUrl}. Rendering ${hasIconUrl ? '<img>' : 'DocumentTextIcon'}.`,
            );
 
            if (!flyer.store) {
              logger.debug(
                `[FlyerList] Flyer ${flyer.flyer_id} has no store. "Unknown Store" fallback will be used.`,
              );
            }
 
            return (
              <li
                data-testid={`flyer-list-item-${flyer.flyer_id}`}
                key={flyer.flyer_id}
                onClick={() => onFlyerSelect(flyer)}
                className={`p-4 flex items-center space-x-3 cursor-pointer transition-colors duration-200 ${selectedFlyerId === flyer.flyer_id ? 'bg-brand-light dark:bg-brand-dark/30' : 'hover:bg-gray-50 dark:hover:bg-gray-800'}`}
                title={tooltipText}
              >
                {flyer.icon_url ? (
                  <img
                    src={flyer.icon_url}
                    alt="Flyer Icon"
                    className="w-6 h-6 shrink-0 rounded-sm"
                  />
                ) : (
                  <DocumentTextIcon className="w-6 h-6 text-brand-primary shrink-0" />
                )}
                <div className="grow min-w-0">
                  <div className="flex items-center space-x-2">
                    <p className="text-sm font-semibold text-gray-900 dark:text-white truncate">
                      {flyer.store?.name || 'Unknown Store'}
                    </p>
                    {flyer.store_address && (
                      <a
                        href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(flyer.store_address)}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        onClick={(e) => e.stopPropagation()}
                        title={`View address: ${flyer.store_address}`}
                      >
                        <MapPinIcon className="w-5 h-5 text-gray-400 hover:text-brand-primary transition-colors" />
                      </a>
                    )}
                  </div>
                  <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
                    {`${flyer.item_count} items`}
                    {dateRange && ` • Valid: ${dateRange}`}
                    {daysLeftText && (
                      <span className={`ml-1 ${daysLeftColor}`}>• {daysLeftText}</span>
                    )}
                  </p>
                </div>
                {isAdmin && (
                  <button
                    onClick={(e) => handleCleanupClick(e, flyer.flyer_id)}
                    className="shrink-0 p-2 rounded-md hover:bg-red-100 dark:hover:bg-red-900/50 text-gray-400 hover:text-red-600 transition-colors"
                    title={`Clean up files for flyer ID ${flyer.flyer_id}`}
                  >
                    <Trash2Icon className="w-4 h-4" />
                  </button>
                )}
              </li>
            );
          })}
        </ul>
      ) : (
        <p className="p-4 text-sm text-gray-500 dark:text-gray-400">
          No flyers have been processed yet. Upload one to get started.
        </p>
      )}
    </div>
  );
};