added sale item count to db, and to be shown in "Flyers" area
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m22s

This commit is contained in:
2025-12-03 10:35:21 -08:00
parent 5f3de95d0e
commit 29484b61c2
9 changed files with 62 additions and 13 deletions

View File

@@ -263,6 +263,28 @@ CREATE TRIGGER on_new_recipe_created
WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes. WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes.
EXECUTE FUNCTION public.log_new_recipe(); EXECUTE FUNCTION public.log_new_recipe();
-- 7a. Trigger function to update the item_count on the flyers table.
DROP FUNCTION IF EXISTS public.update_flyer_item_count();
CREATE OR REPLACE FUNCTION public.update_flyer_item_count()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
UPDATE public.flyers SET item_count = item_count + 1 WHERE flyer_id = NEW.flyer_id;
ELSIF (TG_OP = 'DELETE') THEN
UPDATE public.flyers SET item_count = item_count - 1 WHERE flyer_id = OLD.flyer_id;
END IF;
RETURN NULL; -- The result is ignored since this is an AFTER trigger.
END;
$$ LANGUAGE plpgsql;
-- Trigger to call the function after any change to flyer_items.
-- This ensures the item_count on the parent flyer is always accurate.
DROP TRIGGER IF EXISTS on_flyer_item_change ON public.flyer_items;
CREATE TRIGGER on_flyer_item_change
AFTER INSERT OR DELETE ON public.flyer_items
FOR EACH ROW EXECUTE FUNCTION public.update_flyer_item_count();
-- 7. Trigger function to log the creation of a new flyer. -- 7. Trigger function to log the creation of a new flyer.
DROP FUNCTION IF EXISTS public.log_new_flyer(); DROP FUNCTION IF EXISTS public.log_new_flyer();

View File

@@ -97,6 +97,7 @@ CREATE TABLE IF NOT EXISTS public.flyers (
valid_from DATE, valid_from DATE,
valid_to DATE, valid_to DATE,
store_address TEXT, store_address TEXT,
item_count INTEGER DEFAULT 0 NOT NULL,
uploaded_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL, uploaded_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
@@ -111,6 +112,7 @@ COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a
COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.';
COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.';
COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.'; COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.';
COMMENT ON COLUMN public.flyers.item_count IS 'A cached count of the number of items in this flyer, maintained by a trigger.';
COMMENT ON COLUMN public.flyers.uploaded_by IS 'The user who uploaded the flyer. Can be null for anonymous or system uploads.'; COMMENT ON COLUMN public.flyers.uploaded_by IS 'The user who uploaded the flyer. Can be null for anonymous or system uploads.';
CREATE INDEX IF NOT EXISTS idx_flyers_created_at ON public.flyers (created_at DESC); CREATE INDEX IF NOT EXISTS idx_flyers_created_at ON public.flyers (created_at DESC);

View File

@@ -114,6 +114,7 @@ CREATE TABLE IF NOT EXISTS public.flyers (
valid_from DATE, valid_from DATE,
valid_to DATE, valid_to DATE,
store_address TEXT, store_address TEXT,
item_count INTEGER DEFAULT 0 NOT NULL,
uploaded_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL, uploaded_by UUID REFERENCES public.users(user_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
@@ -128,6 +129,7 @@ COMMENT ON COLUMN public.flyers.store_id IS 'Foreign key linking this flyer to a
COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.valid_from IS 'The start date of the sale period for this flyer, extracted by the AI.';
COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.'; COMMENT ON COLUMN public.flyers.valid_to IS 'The end date of the sale period for this flyer, extracted by the AI.';
COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.'; COMMENT ON COLUMN public.flyers.store_address IS 'The physical store address if it was successfully extracted from the flyer image.';
COMMENT ON COLUMN public.flyers.item_count IS 'A cached count of the number of items in this flyer, maintained by a trigger.';
COMMENT ON COLUMN public.flyers.uploaded_by IS 'The user who uploaded the flyer. Can be null for anonymous or system uploads.'; COMMENT ON COLUMN public.flyers.uploaded_by IS 'The user who uploaded the flyer. Can be null for anonymous or system uploads.';
CREATE INDEX IF NOT EXISTS idx_flyers_created_at ON public.flyers (created_at DESC); CREATE INDEX IF NOT EXISTS idx_flyers_created_at ON public.flyers (created_at DESC);
@@ -2392,6 +2394,28 @@ CREATE TRIGGER on_new_recipe_created
WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes. WHEN (NEW.user_id IS NOT NULL) -- Only log activity for user-created recipes.
EXECUTE FUNCTION public.log_new_recipe(); EXECUTE FUNCTION public.log_new_recipe();
-- 7a. Trigger function to update the item_count on the flyers table.
DROP FUNCTION IF EXISTS public.update_flyer_item_count();
CREATE OR REPLACE FUNCTION public.update_flyer_item_count()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
UPDATE public.flyers SET item_count = item_count + 1 WHERE flyer_id = NEW.flyer_id;
ELSIF (TG_OP = 'DELETE') THEN
UPDATE public.flyers SET item_count = item_count - 1 WHERE flyer_id = OLD.flyer_id;
END IF;
RETURN NULL; -- The result is ignored since this is an AFTER trigger.
END;
$$ LANGUAGE plpgsql;
-- Trigger to call the function after any change to flyer_items.
-- This ensures the item_count on the parent flyer is always accurate.
DROP TRIGGER IF EXISTS on_flyer_item_change ON public.flyer_items;
CREATE TRIGGER on_flyer_item_change
AFTER INSERT OR DELETE ON public.flyer_items
FOR EACH ROW EXECUTE FUNCTION public.update_flyer_item_count();
-- 7. Trigger function to log the creation of a new flyer. -- 7. Trigger function to log the creation of a new flyer.
DROP FUNCTION IF EXISTS public.log_new_flyer(); DROP FUNCTION IF EXISTS public.log_new_flyer();

View File

@@ -36,6 +36,8 @@ export const FlyerList: React.FC<FlyerListProps> = ({ flyers, onFlyerSelect, sel
const from = formatShortDate(flyer.valid_from); const from = formatShortDate(flyer.valid_from);
const to = formatShortDate(flyer.valid_to); const to = formatShortDate(flyer.valid_to);
const dateRange = from && to ? (from === to ? from : `${from} - ${to}`) : from || to; const dateRange = from && to ? (from === to ? from : `${from} - ${to}`) : from || to;
const processedDate = format(parseISO(flyer.created_at), 'P');
const tooltipText = `File: ${flyer.file_name}\nProcessed: ${processedDate}`;
return ( return (
<li <li
@@ -43,22 +45,16 @@ export const FlyerList: React.FC<FlyerListProps> = ({ flyers, onFlyerSelect, sel
key={flyer.flyer_id} key={flyer.flyer_id}
onClick={() => onFlyerSelect(flyer)} onClick={() => onFlyerSelect(flyer)}
className={`group 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'}`} className={`group 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}
> >
<DocumentTextIcon className="w-6 h-6 text-brand-primary shrink-0" /> <DocumentTextIcon className="w-6 h-6 text-brand-primary shrink-0" />
<div className="grow min-w-0"> <div className="grow min-w-0">
<p className="text-sm font-semibold text-gray-900 dark:text-white truncate" title={flyer.store?.name || 'Unknown Store'}> <p className="text-sm font-semibold text-gray-900 dark:text-white truncate" title={flyer.store?.name || 'Unknown Store'}>
{flyer.store?.name || 'Unknown Store'} {flyer.store?.name || 'Unknown Store'}
</p> </p>
{/* The filename is now only visible on hover by default */}
<p className="text-sm text-gray-600 dark:text-gray-400 truncate opacity-0 group-hover:opacity-100 transition-opacity" title={flyer.file_name}>
{flyer.file_name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{/* The "Processed" date is now only visible on hover */} {`${flyer.item_count} items`}
<span className="opacity-0 group-hover:opacity-100 transition-opacity"> {dateRange && ` • Valid: ${dateRange}`}
{`Processed: ${format(parseISO(flyer.created_at), 'P')}`}
</span>
{dateRange && `Valid: ${dateRange}`}
</p> </p>
</div> </div>
</li> </li>

View File

@@ -74,7 +74,8 @@ describe('AI Routes (/api/ai)', () => {
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
file_name: mockDataPayload.originalFileName, file_name: mockDataPayload.originalFileName,
image_url: '/assets/some-image.jpg', image_url: '/assets/some-image.jpg',
...mockDataPayload.extractedData ...mockDataPayload.extractedData,
item_count: 0, // Add missing property to satisfy the Flyer type
}); });
mockedDb.logActivity.mockResolvedValue(); mockedDb.logActivity.mockResolvedValue();

View File

@@ -23,7 +23,7 @@ const errMsg = (e: unknown) => {
}; };
// --- Multer Configuration for File Uploads --- // --- Multer Configuration for File Uploads ---
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets'; const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images';
// Ensure the storage path exists at startup so multer can write files there. // Ensure the storage path exists at startup so multer can write files there.
try { try {
@@ -243,6 +243,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
valid_from: extractedData.valid_from, valid_from: extractedData.valid_from,
valid_to: extractedData.valid_to, valid_to: extractedData.valid_to,
store_address: extractedData.store_address, store_address: extractedData.store_address,
item_count: 0, // Set default to 0; the trigger will update it.
uploaded_by: user?.user_id, // Associate with user if logged in uploaded_by: user?.user_id, // Associate with user if logged in
}; };

View File

@@ -22,6 +22,7 @@ export async function getFlyers(): Promise<Flyer[]> {
f.valid_from, f.valid_from,
f.valid_to, f.valid_to,
f.store_address, f.store_address,
f.item_count,
json_build_object( json_build_object(
'store_id', s.store_id, 'store_id', s.store_id,
'name', s.name, 'name', s.name,

View File

@@ -133,13 +133,14 @@ export const flyerWorker = new Worker<FlyerJobData>(
const flyerData = { const flyerData = {
file_name: originalFileName, file_name: originalFileName,
image_url: `/assets/${path.basename(firstImage)}`, image_url: `/flyer-images/${path.basename(firstImage)}`,
icon_url: `/assets/icons/${iconFileName}`, icon_url: `/flyer-images/icons/${iconFileName}`,
checksum, checksum,
store_name: extractedData.store_name || 'Unknown Store (auto)', store_name: extractedData.store_name || 'Unknown Store (auto)',
valid_from: extractedData.valid_from, valid_from: extractedData.valid_from,
valid_to: extractedData.valid_to, valid_to: extractedData.valid_to,
store_address: extractedData.store_address, store_address: extractedData.store_address,
item_count: 0, // Set default to 0; the trigger will update it.
uploaded_by: userId, uploaded_by: userId,
}; };

View File

@@ -17,6 +17,7 @@ export interface Flyer {
valid_from?: string | null; valid_from?: string | null;
valid_to?: string | null; valid_to?: string | null;
store_address?: string | null; store_address?: string | null;
item_count: number;
uploaded_by?: string | null; // UUID of the user who uploaded it, can be null for anonymous uploads uploaded_by?: string | null; // UUID of the user who uploaded it, can be null for anonymous uploads
store?: Store; store?: Store;
} }