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
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m22s
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user