imported files from google gemini ai BUILD env
This commit is contained in:
187
components/ExtractedDataTable.tsx
Normal file
187
components/ExtractedDataTable.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { FlyerItem, MasterGroceryItem, ShoppingList } from '../types';
|
||||
import { formatUnitPrice } from '../utils/unitConverter';
|
||||
import { Session } from '@supabase/supabase-js';
|
||||
import { PlusCircleIcon } from './icons/PlusCircleIcon';
|
||||
|
||||
interface ExtractedDataTableProps {
|
||||
items: FlyerItem[];
|
||||
totalActiveItems?: number;
|
||||
watchedItems?: MasterGroceryItem[];
|
||||
masterItems: MasterGroceryItem[];
|
||||
unitSystem: 'metric' | 'imperial';
|
||||
session: Session | null;
|
||||
onAddItem: (itemName: string, category: string) => Promise<void>;
|
||||
shoppingLists: ShoppingList[];
|
||||
activeListId: number | null;
|
||||
onAddItemToList: (masterItemId: number) => void;
|
||||
}
|
||||
|
||||
export const ExtractedDataTable: React.FC<ExtractedDataTableProps> = ({ items, totalActiveItems, watchedItems = [], masterItems, unitSystem, session, onAddItem, shoppingLists, activeListId, onAddItemToList }) => {
|
||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||
|
||||
const watchedItemIds = useMemo(() => new Set(watchedItems.map(item => item.id)), [watchedItems]);
|
||||
const masterItemsMap = useMemo(() => new Map(masterItems.map(item => [item.id, item.name])), [masterItems]);
|
||||
|
||||
const activeShoppingListItems = useMemo(() => {
|
||||
if (!activeListId) return new Set();
|
||||
const activeList = shoppingLists.find(list => list.id === activeListId);
|
||||
return new Set(activeList?.items.map(item => item.master_item_id));
|
||||
}, [shoppingLists, activeListId]);
|
||||
|
||||
const availableCategories = useMemo(() => {
|
||||
const cats = new Set(items.map(i => i.category_name).filter((c): c is string => !!c));
|
||||
return Array.from(cats).sort();
|
||||
}, [items]);
|
||||
|
||||
const itemsWithCanonicalNames = useMemo(() => {
|
||||
return items.map(item => ({
|
||||
...item,
|
||||
resolved_canonical_name: item.master_item_id ? masterItemsMap.get(item.master_item_id) : null,
|
||||
}));
|
||||
}, [items, masterItemsMap]);
|
||||
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
const filtered = categoryFilter === 'all'
|
||||
? itemsWithCanonicalNames
|
||||
: itemsWithCanonicalNames.filter(item => item.category_name === categoryFilter);
|
||||
|
||||
if (watchedItemIds.size === 0) {
|
||||
return filtered;
|
||||
}
|
||||
const watched = [];
|
||||
const others = [];
|
||||
for (const item of filtered) {
|
||||
const isWatched = item.master_item_id && watchedItemIds.has(item.master_item_id);
|
||||
if (isWatched) {
|
||||
watched.push(item);
|
||||
} else {
|
||||
others.push(item);
|
||||
}
|
||||
}
|
||||
return [...watched, ...others];
|
||||
}, [itemsWithCanonicalNames, watchedItemIds, categoryFilter]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="text-center p-8 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<p className="text-gray-500">No items extracted yet.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const title = (totalActiveItems && totalActiveItems > 0)
|
||||
? `Item List (${items.length} in flyer / ${totalActiveItems} total active deals)`
|
||||
: `Item List (${items.length})`;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
{availableCategories.length > 1 && (
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="block pl-3 pr-8 py-1.5 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary"
|
||||
aria-label="Filter by category"
|
||||
>
|
||||
<option value="all">All Categories</option>
|
||||
{availableCategories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{sortedItems.length === 0 ? (
|
||||
<div className="text-center p-8 text-gray-500 dark:text-gray-400">
|
||||
No items found for the selected category.
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full">
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{sortedItems.map((item, index) => {
|
||||
const canonicalName = item.resolved_canonical_name;
|
||||
const isWatched = item.master_item_id && watchedItemIds.has(item.master_item_id);
|
||||
const isInList = !!(item.master_item_id && activeShoppingListItems.has(item.master_item_id));
|
||||
|
||||
const itemNameClass = isWatched
|
||||
? 'text-sm font-bold text-green-600 dark:text-green-400'
|
||||
: 'text-sm font-semibold text-gray-900 dark:text-white';
|
||||
|
||||
const shouldShowCanonical = canonicalName && canonicalName.toLowerCase() !== item.item.toLowerCase();
|
||||
const formattedUnitPrice = formatUnitPrice(item.unit_price, unitSystem);
|
||||
|
||||
return (
|
||||
<tr key={item.id || `${item.item}-${index}`} className="group hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td className="px-6 py-4 whitespace-normal">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className={itemNameClass}>{item.item}</div>
|
||||
<div className="flex items-center space-x-2 flex-shrink-0 ml-4">
|
||||
{session && canonicalName && !isInList && (
|
||||
<button
|
||||
onClick={() => onAddItemToList(item.master_item_id!)}
|
||||
disabled={!activeListId}
|
||||
className="text-gray-400 hover:text-brand-primary disabled:text-gray-300 disabled:cursor-not-allowed dark:text-gray-500 dark:hover:text-brand-light transition-colors"
|
||||
title={activeListId ? `Add ${canonicalName} to list` : 'Select a shopping list first'}
|
||||
>
|
||||
<PlusCircleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{session && !isWatched && canonicalName && (
|
||||
<button
|
||||
onClick={() => onAddItem(canonicalName, item.category_name || 'Other/Miscellaneous')}
|
||||
className="text-xs bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-brand-primary dark:text-brand-light font-semibold py-1 px-2.5 rounded-md transition-colors duration-200"
|
||||
title={`Add '${canonicalName}' to your watchlist`}
|
||||
>
|
||||
+ Watch
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="font-medium text-gray-500 w-16 shrink-0">Price:</span>
|
||||
<span>{item.price_display}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="font-medium text-gray-500 w-16 shrink-0">Deal:</span>
|
||||
<div className="flex items-baseline">
|
||||
<span>{item.quantity}</span>
|
||||
{item.quantity_num && <span className="ml-1.5 text-gray-400 dark:text-gray-500">({item.quantity_num})</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="font-medium text-gray-500 w-16 shrink-0">Unit Price:</span>
|
||||
<div className="flex items-baseline">
|
||||
<span className="font-semibold text-gray-700 dark:text-gray-300">
|
||||
{formattedUnitPrice.price}
|
||||
</span>
|
||||
{formattedUnitPrice.unit && (
|
||||
<span className="ml-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formattedUnitPrice.unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="font-medium text-gray-500 w-16 shrink-0">Category:</span>
|
||||
<span className="italic">{item.category_name}</span>
|
||||
{shouldShowCanonical && (
|
||||
<span className="ml-4 italic text-gray-400">(Canonical: {canonicalName})</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user