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>
);
};
|