diff --git a/supabase/migrations/003_helper_functions.sql b/supabase/migrations/003_helper_functions.sql new file mode 100644 index 0000000..db6267d --- /dev/null +++ b/supabase/migrations/003_helper_functions.sql @@ -0,0 +1,251 @@ +-- Migration: Additional SQL Functions for Inventory Management +-- Week 2: Helper functions for common queries + +-- Function: Get inventory items with full details (tags, product info, unit conversion) +CREATE OR REPLACE FUNCTION get_inventory_details() +RETURNS TABLE ( + item_id UUID, + item_name TEXT, + quantity DECIMAL, + unit_abbreviation TEXT, + unit_name TEXT, + expiry_date DATE, + days_until_expiry INTEGER, + tags TEXT[], + product_brand TEXT, + product_image_url TEXT, + product_barcode TEXT, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +) AS $$ +BEGIN + RETURN QUERY + SELECT + i.id AS item_id, + i.name AS item_name, + i.quantity, + u.abbreviation AS unit_abbreviation, + u.name AS unit_name, + i.expiry_date, + (i.expiry_date - CURRENT_DATE) AS days_until_expiry, + COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags, + p.brand AS product_brand, + p.image_url AS product_image_url, + p.barcode AS product_barcode, + i.created_at, + i.updated_at + FROM inventory_items i + JOIN units u ON i.unit_id = u.id + LEFT JOIN products p ON i.product_id = p.id + LEFT JOIN item_tags it ON i.id = it.item_id + LEFT JOIN tags t ON it.tag_id = t.id + GROUP BY + i.id, i.name, i.quantity, u.abbreviation, u.name, + i.expiry_date, p.brand, p.image_url, p.barcode, + i.created_at, i.updated_at + ORDER BY i.created_at DESC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION get_inventory_details() IS 'Returns all inventory items with denormalized data for display'; + +-- Function: Get items expiring soon +CREATE OR REPLACE FUNCTION get_expiring_items(days_ahead INTEGER DEFAULT 7) +RETURNS TABLE ( + item_id UUID, + item_name TEXT, + quantity DECIMAL, + unit_abbreviation TEXT, + expiry_date DATE, + days_until_expiry INTEGER, + tags TEXT[] +) AS $$ +BEGIN + RETURN QUERY + SELECT + i.id AS item_id, + i.name AS item_name, + i.quantity, + u.abbreviation AS unit_abbreviation, + i.expiry_date, + (i.expiry_date - CURRENT_DATE) AS days_until_expiry, + COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags + FROM inventory_items i + JOIN units u ON i.unit_id = u.id + LEFT JOIN item_tags it ON i.id = it.item_id + LEFT JOIN tags t ON it.tag_id = t.id + WHERE + i.expiry_date IS NOT NULL + AND i.expiry_date <= CURRENT_DATE + MAKE_INTERVAL(days => days_ahead) + GROUP BY i.id, i.name, i.quantity, u.abbreviation, i.expiry_date + ORDER BY i.expiry_date ASC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION get_expiring_items(INTEGER) IS 'Returns items expiring within specified days (default 7)'; + +-- Function: Get items by tag +CREATE OR REPLACE FUNCTION get_items_by_tag(tag_name TEXT) +RETURNS TABLE ( + item_id UUID, + item_name TEXT, + quantity DECIMAL, + unit_abbreviation TEXT, + expiry_date DATE, + created_at TIMESTAMPTZ +) AS $$ +BEGIN + RETURN QUERY + SELECT + i.id AS item_id, + i.name AS item_name, + i.quantity, + u.abbreviation AS unit_abbreviation, + i.expiry_date, + i.created_at + FROM inventory_items i + JOIN units u ON i.unit_id = u.id + JOIN item_tags it ON i.id = it.item_id + JOIN tags t ON it.tag_id = t.id + WHERE t.name ILIKE tag_name + ORDER BY i.created_at DESC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION get_items_by_tag(TEXT) IS 'Returns all items with specified tag (case-insensitive)'; + +-- Function: Get low stock items (quantity <= threshold) +CREATE OR REPLACE FUNCTION get_low_stock_items(threshold DECIMAL DEFAULT 1.0) +RETURNS TABLE ( + item_id UUID, + item_name TEXT, + quantity DECIMAL, + unit_abbreviation TEXT, + tags TEXT[] +) AS $$ +BEGIN + RETURN QUERY + SELECT + i.id AS item_id, + i.name AS item_name, + i.quantity, + u.abbreviation AS unit_abbreviation, + COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags + FROM inventory_items i + JOIN units u ON i.unit_id = u.id + LEFT JOIN item_tags it ON i.id = it.item_id + LEFT JOIN tags t ON it.tag_id = t.id + WHERE i.quantity <= threshold + GROUP BY i.id, i.name, i.quantity, u.abbreviation + ORDER BY i.quantity ASC, i.name ASC; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION get_low_stock_items(DECIMAL) IS 'Returns items with quantity at or below threshold'; + +-- Function: Update item quantity (consume or restock) +CREATE OR REPLACE FUNCTION update_item_quantity( + item_uuid UUID, + quantity_change DECIMAL, + delete_if_zero BOOLEAN DEFAULT TRUE +) +RETURNS BOOLEAN AS $$ +DECLARE + new_quantity DECIMAL; +BEGIN + -- Calculate new quantity + SELECT quantity + quantity_change INTO new_quantity + FROM inventory_items + WHERE id = item_uuid; + + IF new_quantity IS NULL THEN + RETURN FALSE; -- Item not found + END IF; + + -- Delete if zero and flag is set + IF new_quantity <= 0 AND delete_if_zero THEN + DELETE FROM inventory_items WHERE id = item_uuid; + RETURN TRUE; + END IF; + + -- Update quantity (ensure non-negative) + UPDATE inventory_items + SET quantity = GREATEST(new_quantity, 0), + updated_at = NOW() + WHERE id = item_uuid; + + RETURN TRUE; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION update_item_quantity(UUID, DECIMAL, BOOLEAN) IS 'Updates item quantity (positive for restock, negative for consume). Optionally deletes if zero.'; + +-- Function: Get inventory statistics +CREATE OR REPLACE FUNCTION get_inventory_stats() +RETURNS TABLE ( + total_items BIGINT, + total_unique_products BIGINT, + items_expiring_week BIGINT, + items_expired BIGINT, + total_tags_used BIGINT +) AS $$ +BEGIN + RETURN QUERY + SELECT + COUNT(DISTINCT i.id) AS total_items, + COUNT(DISTINCT i.product_id) FILTER (WHERE i.product_id IS NOT NULL) AS total_unique_products, + COUNT(i.id) FILTER ( + WHERE i.expiry_date IS NOT NULL + AND i.expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days' + ) AS items_expiring_week, + COUNT(i.id) FILTER ( + WHERE i.expiry_date IS NOT NULL + AND i.expiry_date < CURRENT_DATE + ) AS items_expired, + COUNT(DISTINCT it.tag_id) AS total_tags_used + FROM inventory_items i + LEFT JOIN item_tags it ON i.id = it.item_id; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION get_inventory_stats() IS 'Returns summary statistics for the entire inventory'; + +-- Function: Search inventory (full-text search on items and products) +CREATE OR REPLACE FUNCTION search_inventory(search_query TEXT) +RETURNS TABLE ( + item_id UUID, + item_name TEXT, + quantity DECIMAL, + unit_abbreviation TEXT, + product_brand TEXT, + tags TEXT[], + relevance REAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + i.id AS item_id, + i.name AS item_name, + i.quantity, + u.abbreviation AS unit_abbreviation, + p.brand AS product_brand, + COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags, + ts_rank( + to_tsvector('english', i.name || ' ' || COALESCE(p.brand, '') || ' ' || COALESCE(p.name, '')), + plainto_tsquery('english', search_query) + ) AS relevance + FROM inventory_items i + JOIN units u ON i.unit_id = u.id + LEFT JOIN products p ON i.product_id = p.id + LEFT JOIN item_tags it ON i.id = it.item_id + LEFT JOIN tags t ON it.tag_id = t.id + WHERE + to_tsvector('english', i.name || ' ' || COALESCE(p.brand, '') || ' ' || COALESCE(p.name, '')) + @@ plainto_tsquery('english', search_query) + GROUP BY i.id, i.name, i.quantity, u.abbreviation, p.brand, p.name + ORDER BY relevance DESC, i.created_at DESC + LIMIT 50; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION search_inventory(TEXT) IS 'Full-text search across inventory items and products';