|
|
|
|
@@ -17,6 +17,39 @@ export let supabase = (supabaseUrl && supabaseAnonKey)
|
|
|
|
|
? createClient<Database>(supabaseUrl, supabaseAnonKey)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// INTERNAL HELPERS
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A centralized check to ensure the Supabase client is initialized before use.
|
|
|
|
|
* @throws {Error} If the client is not initialized.
|
|
|
|
|
*/
|
|
|
|
|
const ensureSupabase = (): SupabaseClient<Database> => {
|
|
|
|
|
if (!supabase) {
|
|
|
|
|
throw new Error("Supabase client not initialized. Please check your environment variables and configuration.");
|
|
|
|
|
}
|
|
|
|
|
return supabase;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generic helper to handle Supabase query responses, centralizing error handling and data casting.
|
|
|
|
|
* @param query A Supabase query promise.
|
|
|
|
|
* @returns The data from the query, or throws an error.
|
|
|
|
|
*/
|
|
|
|
|
const handleResponse = async <T>(query: PromiseLike<{ data: T | null; error: any }>): Promise<T> => {
|
|
|
|
|
const { data, error } = await query;
|
|
|
|
|
if (error) {
|
|
|
|
|
throw new Error(error.message);
|
|
|
|
|
}
|
|
|
|
|
if (data === null) {
|
|
|
|
|
// This case can happen with .single() if no row is found.
|
|
|
|
|
// For list queries, an empty array is returned instead of null.
|
|
|
|
|
throw new Error("Query returned no data, but no error was reported.");
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Disconnects the Supabase client by setting the instance to null.
|
|
|
|
|
*/
|
|
|
|
|
@@ -32,13 +65,12 @@ export const disconnectSupabase = () => {
|
|
|
|
|
* @returns An object indicating success and any error message.
|
|
|
|
|
*/
|
|
|
|
|
export const testDatabaseConnection = async (): Promise<{ success: boolean; error: string | null }> => {
|
|
|
|
|
if (!supabase) return { success: false, error: 'Supabase client not initialized.' };
|
|
|
|
|
try {
|
|
|
|
|
const { error } = await supabase.from('stores').select('id').limit(1);
|
|
|
|
|
const { error } = await ensureSupabase().from('stores').select('id').limit(1);
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
return { success: true, error: null };
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
return { success: false, error: `Database connection test failed: ${error.message}. Check RLS policies.` };
|
|
|
|
|
return { success: false, error: `Database connection test failed: ${error.message}. Check RLS policies and client initialization.` };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -47,7 +79,7 @@ export const testDatabaseConnection = async (): Promise<{ success: boolean; erro
|
|
|
|
|
* @returns An object indicating success and any error message.
|
|
|
|
|
*/
|
|
|
|
|
export const runDatabaseSelfTest = async (): Promise<{ success: boolean; error: string | null }> => {
|
|
|
|
|
if (!supabase) return { success: false, error: 'Supabase client not initialized.' };
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const testItem = {
|
|
|
|
|
item: `DB_SELF_TEST_ITEM_${Date.now()}`,
|
|
|
|
|
@@ -57,7 +89,7 @@ export const runDatabaseSelfTest = async (): Promise<{ success: boolean; error:
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. Insert
|
|
|
|
|
const { data: insertData, error: insertError } = await supabase
|
|
|
|
|
const { data: insertData, error: insertError } = await supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.insert(testItem)
|
|
|
|
|
.select()
|
|
|
|
|
@@ -69,14 +101,14 @@ export const runDatabaseSelfTest = async (): Promise<{ success: boolean; error:
|
|
|
|
|
const testItemId = insertData.id;
|
|
|
|
|
|
|
|
|
|
// 3. Update
|
|
|
|
|
const { error: updateError } = await supabase
|
|
|
|
|
const { error: updateError } = await supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.update({ item: 'DB_SELF_TEST_ITEM_UPDATED' })
|
|
|
|
|
.eq('id', testItemId);
|
|
|
|
|
if (updateError) throw new Error(`Update failed: ${updateError.message}`);
|
|
|
|
|
|
|
|
|
|
// 4. Delete
|
|
|
|
|
const { error: deleteError } = await supabase
|
|
|
|
|
const { error: deleteError } = await supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.delete()
|
|
|
|
|
.eq('id', testItemId);
|
|
|
|
|
@@ -93,7 +125,7 @@ export const runDatabaseSelfTest = async (): Promise<{ success: boolean; error:
|
|
|
|
|
* @returns An object indicating success and any error message.
|
|
|
|
|
*/
|
|
|
|
|
export const testStorageConnection = async (): Promise<{ success: boolean; error: string | null }> => {
|
|
|
|
|
if (!supabase) return { success: false, error: 'Supabase client not initialized.' };
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const bucketName = 'flyers';
|
|
|
|
|
const testFileName = `storage-self-test-${Date.now()}.txt`;
|
|
|
|
|
@@ -101,13 +133,13 @@ export const testStorageConnection = async (): Promise<{ success: boolean; error
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. Upload
|
|
|
|
|
const { error: uploadError } = await supabase.storage
|
|
|
|
|
const { error: uploadError } = await supabaseClient.storage
|
|
|
|
|
.from(bucketName)
|
|
|
|
|
.upload(testFileName, testFileContent);
|
|
|
|
|
if (uploadError) throw new Error(`Upload to storage failed: ${uploadError.message}`);
|
|
|
|
|
|
|
|
|
|
// 2. Delete
|
|
|
|
|
const { error: deleteError } = await supabase.storage
|
|
|
|
|
const { error: deleteError } = await supabaseClient.storage
|
|
|
|
|
.from(bucketName)
|
|
|
|
|
.remove([testFileName]);
|
|
|
|
|
if (deleteError) throw new Error(`Deleting from storage failed: ${deleteError.message}`);
|
|
|
|
|
@@ -124,15 +156,17 @@ export const testStorageConnection = async (): Promise<{ success: boolean; error
|
|
|
|
|
* @returns The public URL of the uploaded image.
|
|
|
|
|
*/
|
|
|
|
|
export const uploadFlyerImage = async (file: File): Promise<string> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const fileName = `${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`;
|
|
|
|
|
const { data, error } = await supabase.storage
|
|
|
|
|
const { data: uploadData, error } = await supabaseClient.storage
|
|
|
|
|
.from('flyers')
|
|
|
|
|
.upload(fileName, file);
|
|
|
|
|
if (error) throw new Error(`Failed to upload flyer image: ${error.message}`);
|
|
|
|
|
if (!uploadData) throw new Error("Upload did not return a path.");
|
|
|
|
|
|
|
|
|
|
const { data: { publicUrl } } = supabase.storage.from('flyers').getPublicUrl(data.path);
|
|
|
|
|
const { data: urlData } = supabaseClient.storage.from('flyers').getPublicUrl(uploadData.path);
|
|
|
|
|
const publicUrl = urlData?.publicUrl;
|
|
|
|
|
if (!publicUrl) throw new Error("Could not get public URL for uploaded flyer image.");
|
|
|
|
|
|
|
|
|
|
return publicUrl;
|
|
|
|
|
@@ -151,22 +185,22 @@ export const createFlyerRecord = async (
|
|
|
|
|
validTo: string | null,
|
|
|
|
|
storeAddress: string | null
|
|
|
|
|
): Promise<Flyer> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
let { data: store } = await supabase
|
|
|
|
|
let store = await handleResponse(
|
|
|
|
|
supabaseClient
|
|
|
|
|
.from('stores')
|
|
|
|
|
.select('*')
|
|
|
|
|
.ilike('name', storeName)
|
|
|
|
|
.single();
|
|
|
|
|
.maybeSingle() // Use maybeSingle to gracefully handle no-store-found case
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!store) {
|
|
|
|
|
const { data: newStore, error: newStoreError } = await supabase
|
|
|
|
|
store = await handleResponse(supabaseClient
|
|
|
|
|
.from('stores')
|
|
|
|
|
.insert({ name: storeName })
|
|
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
if (newStoreError) throw new Error(`Error creating store: ${newStoreError.message}`);
|
|
|
|
|
store = newStore;
|
|
|
|
|
.single());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { data: newFlyer, error: flyerError } = await supabase
|
|
|
|
|
@@ -196,7 +230,7 @@ export const createFlyerRecord = async (
|
|
|
|
|
* @returns The array of saved items with their new IDs.
|
|
|
|
|
*/
|
|
|
|
|
export const saveFlyerItems = async (items: Omit<FlyerItem, 'id' | 'created_at' | 'flyer_id'>[], flyerId: number): Promise<FlyerItem[]> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
if (items.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const itemsToInsert = items.map(item => {
|
|
|
|
|
@@ -205,7 +239,7 @@ export const saveFlyerItems = async (items: Omit<FlyerItem, 'id' | 'created_at'
|
|
|
|
|
return { ...rest, flyer_id: flyerId };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: savedItems, error } = await supabase
|
|
|
|
|
const { data: savedItems, error } = await supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
// We cast to `any` here to bypass the strict type check on the `unit_price` (Json vs UnitPrice)
|
|
|
|
|
// This is safe because UnitPrice is a valid JSONB structure.
|
|
|
|
|
@@ -222,16 +256,14 @@ export const saveFlyerItems = async (items: Omit<FlyerItem, 'id' | 'created_at'
|
|
|
|
|
* @returns An array of flyer objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getFlyers = async (): Promise<Flyer[]> => {
|
|
|
|
|
if (!supabase) return [];
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('flyers')
|
|
|
|
|
.select('*, store:stores(*)')
|
|
|
|
|
.order('created_at', { ascending: false });
|
|
|
|
|
if (error) throw new Error(`Failed to get flyers: ${error.message}`);
|
|
|
|
|
.order('created_at', { ascending: false }));
|
|
|
|
|
|
|
|
|
|
// Cast the result back to the expected application type
|
|
|
|
|
return (data as Flyer[]) || [];
|
|
|
|
|
return (data as Flyer[]) ?? [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -240,17 +272,15 @@ export const getFlyers = async (): Promise<Flyer[]> => {
|
|
|
|
|
* @returns An array of flyer item objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getFlyerItems = async (flyerId: number): Promise<FlyerItem[]> => {
|
|
|
|
|
if (!supabase) return [];
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.select('*')
|
|
|
|
|
.eq('flyer_id', flyerId)
|
|
|
|
|
.order('item', { ascending: true });
|
|
|
|
|
if (error) throw new Error(`Failed to get flyer items: ${error.message}`);
|
|
|
|
|
.order('item', { ascending: true }));
|
|
|
|
|
|
|
|
|
|
// Cast the result back to the expected application type
|
|
|
|
|
return (data as unknown as FlyerItem[]) || [];
|
|
|
|
|
return (data as unknown as FlyerItem[]) ?? [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -259,9 +289,9 @@ export const getFlyerItems = async (flyerId: number): Promise<FlyerItem[]> => {
|
|
|
|
|
* @returns The found flyer object or null.
|
|
|
|
|
*/
|
|
|
|
|
export const findFlyerByChecksum = async (checksum: string): Promise<Flyer | null> => {
|
|
|
|
|
if (!supabase) return null;
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const { data, error } = await supabaseClient
|
|
|
|
|
.from('flyers')
|
|
|
|
|
.select('*')
|
|
|
|
|
.eq('checksum', checksum)
|
|
|
|
|
@@ -281,10 +311,7 @@ export const findFlyerByChecksum = async (checksum: string): Promise<Flyer | nul
|
|
|
|
|
* @returns The public URL of the logo if successful, otherwise null.
|
|
|
|
|
*/
|
|
|
|
|
export const uploadLogoAndUpdateStore = async (storeId: number, logoBase64: string): Promise<string | null> => {
|
|
|
|
|
if (!supabase) {
|
|
|
|
|
console.warn("Cannot upload logo: Supabase client not initialized.");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Helper function to convert base64 to a Blob for uploading.
|
|
|
|
|
@@ -301,7 +328,7 @@ export const uploadLogoAndUpdateStore = async (storeId: number, logoBase64: stri
|
|
|
|
|
const logoBlob = base64ToBlob(logoBase64, 'image/png');
|
|
|
|
|
const filePath = `logos/store_logo_${storeId}.png`;
|
|
|
|
|
|
|
|
|
|
const { data, error: uploadError } = await supabase.storage
|
|
|
|
|
const { data, error: uploadError } = await supabaseClient.storage
|
|
|
|
|
.from('flyers')
|
|
|
|
|
.upload(filePath, logoBlob, {
|
|
|
|
|
cacheControl: '3600',
|
|
|
|
|
@@ -313,14 +340,15 @@ export const uploadLogoAndUpdateStore = async (storeId: number, logoBase64: stri
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { data: { publicUrl } } = supabase.storage.from('flyers').getPublicUrl(data.path);
|
|
|
|
|
const { data: urlData } = supabaseClient.storage.from('flyers').getPublicUrl(data.path);
|
|
|
|
|
const publicUrl = urlData?.publicUrl;
|
|
|
|
|
if (!publicUrl) {
|
|
|
|
|
console.warn("Could not get public URL for uploaded logo.");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the store record with the new URL, but only if it's currently null.
|
|
|
|
|
const { error: updateError } = await supabase
|
|
|
|
|
const { error: updateError } = await supabaseClient
|
|
|
|
|
.from('stores')
|
|
|
|
|
.update({ logo_url: publicUrl })
|
|
|
|
|
.eq('id', storeId)
|
|
|
|
|
@@ -343,20 +371,20 @@ export const uploadLogoAndUpdateStore = async (storeId: number, logoBase64: stri
|
|
|
|
|
* @returns An array of master grocery item objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getWatchedItems = async (userId: string): Promise<MasterGroceryItem[]> => {
|
|
|
|
|
if (!supabase) return [];
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('user_watched_items')
|
|
|
|
|
.select('master_grocery_items(*, category_name:categories(name))')
|
|
|
|
|
.select(`
|
|
|
|
|
master_item:master_grocery_items (
|
|
|
|
|
*,
|
|
|
|
|
category:categories (name)
|
|
|
|
|
)
|
|
|
|
|
`)
|
|
|
|
|
.eq('user_id', userId)
|
|
|
|
|
.order('name', { ascending: true, referencedTable: 'master_grocery_items' });
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Error fetching watched items: ${error.message}`);
|
|
|
|
|
.order('name', { referencedTable: 'master_grocery_items', ascending: true }));
|
|
|
|
|
|
|
|
|
|
return (data || []).map((item: any) => ({
|
|
|
|
|
...item.master_grocery_items,
|
|
|
|
|
category_name: item.master_grocery_items.category_name?.name,
|
|
|
|
|
}));
|
|
|
|
|
return (data?.map(item => ({ ...item.master_item, category_name: item.master_item.category.name })) ?? []) as MasterGroceryItem[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -364,19 +392,17 @@ export const getWatchedItems = async (userId: string): Promise<MasterGroceryItem
|
|
|
|
|
* @returns An array of master grocery item objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getAllMasterItems = async (): Promise<MasterGroceryItem[]> => {
|
|
|
|
|
if (!supabase) return [];
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('master_grocery_items')
|
|
|
|
|
.select('*, category_name:categories(name)')
|
|
|
|
|
.order('name', { ascending: true });
|
|
|
|
|
.order('name', { ascending: true }));
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Error fetching master items: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
return (data || []).map(item => ({
|
|
|
|
|
return (data?.map(item => ({
|
|
|
|
|
...item,
|
|
|
|
|
category_name: (item.category_name as any)?.name,
|
|
|
|
|
}));
|
|
|
|
|
})) ?? []) as MasterGroceryItem[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -388,49 +414,56 @@ export const getAllMasterItems = async (): Promise<MasterGroceryItem[]> => {
|
|
|
|
|
* @returns The master item object that was added to the watchlist.
|
|
|
|
|
*/
|
|
|
|
|
export const addWatchedItem = async (userId: string, itemName: string, category: string): Promise<MasterGroceryItem> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
// 1. Find or create the category
|
|
|
|
|
let { data: categoryData } = await supabase
|
|
|
|
|
.from('categories')
|
|
|
|
|
.select('id')
|
|
|
|
|
.eq('name', category)
|
|
|
|
|
.single();
|
|
|
|
|
|
|
|
|
|
if (!categoryData) {
|
|
|
|
|
const { data: newCategoryData, error: newCategoryError } = await supabase
|
|
|
|
|
.from('categories')
|
|
|
|
|
.insert({ name: category })
|
|
|
|
|
.select('id')
|
|
|
|
|
try {
|
|
|
|
|
// 1. Find or create the category
|
|
|
|
|
let categoryData = await handleResponse(
|
|
|
|
|
supabaseClient
|
|
|
|
|
.from('categories')
|
|
|
|
|
.select('id')
|
|
|
|
|
.eq('name', category)
|
|
|
|
|
.maybeSingle()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!categoryData) {
|
|
|
|
|
categoryData = await handleResponse(
|
|
|
|
|
supabaseClient
|
|
|
|
|
.from('categories')
|
|
|
|
|
.insert({ name: category })
|
|
|
|
|
.select('id')
|
|
|
|
|
.single()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Upsert the master item to ensure it exists and get its ID
|
|
|
|
|
const { data: masterItem, error: masterItemError } = await supabaseClient
|
|
|
|
|
.from('master_grocery_items')
|
|
|
|
|
.upsert({ name: itemName.trim(), category_id: categoryData.id }, { onConflict: 'name' })
|
|
|
|
|
.select('*, category_name:categories(name)')
|
|
|
|
|
.single();
|
|
|
|
|
if (newCategoryError) throw new Error(`Error creating category: ${newCategoryError.message}`);
|
|
|
|
|
categoryData = newCategoryData;
|
|
|
|
|
if (masterItemError) throw new Error(`Failed to upsert master item: ${masterItemError.message}`);
|
|
|
|
|
if (!masterItem) throw new Error("Master item operation did not return data.");
|
|
|
|
|
|
|
|
|
|
// 3. Create the link in user_watched_items
|
|
|
|
|
const { error: watchLinkError } = await supabaseClient
|
|
|
|
|
.from('user_watched_items')
|
|
|
|
|
.insert({ user_id: userId, master_item_id: masterItem.id });
|
|
|
|
|
|
|
|
|
|
// Ignore duplicate errors (user already watching), throw others
|
|
|
|
|
if (watchLinkError && watchLinkError.code !== '23505') {
|
|
|
|
|
throw new Error(`Failed to add item to watchlist: ${watchLinkError.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Return the full master item object for UI update
|
|
|
|
|
return {
|
|
|
|
|
...masterItem,
|
|
|
|
|
category_name: (masterItem.category_name as any)?.name,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// Centralized error handling for the entire function
|
|
|
|
|
throw new Error(`Failed in addWatchedItem: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Upsert the master item to ensure it exists and get its ID
|
|
|
|
|
const { data: masterItem, error: masterItemError } = await supabase
|
|
|
|
|
.from('master_grocery_items')
|
|
|
|
|
.upsert({ name: itemName.trim(), category_id: categoryData.id }, { onConflict: 'name', ignoreDuplicates: false })
|
|
|
|
|
.select('*, category_name:categories(name)')
|
|
|
|
|
.single();
|
|
|
|
|
if (masterItemError) throw new Error(`Failed to upsert master item: ${masterItemError.message}`);
|
|
|
|
|
if (!masterItem) throw new Error("Master item operation did not return data.");
|
|
|
|
|
|
|
|
|
|
// 3. Create the link in user_watched_items
|
|
|
|
|
const { error: watchLinkError } = await supabase
|
|
|
|
|
.from('user_watched_items')
|
|
|
|
|
.insert({ user_id: userId, master_item_id: masterItem.id });
|
|
|
|
|
|
|
|
|
|
// Ignore duplicate errors (user already watching), throw others
|
|
|
|
|
if (watchLinkError && watchLinkError.code !== '23505') {
|
|
|
|
|
throw new Error(`Failed to add item to watchlist: ${watchLinkError.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Return the full master item object for UI update
|
|
|
|
|
return {
|
|
|
|
|
...masterItem,
|
|
|
|
|
category_name: (masterItem.category_name as any)?.name,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -439,9 +472,9 @@ export const addWatchedItem = async (userId: string, itemName: string, category:
|
|
|
|
|
* @param masterItemId The ID of the master item to remove.
|
|
|
|
|
*/
|
|
|
|
|
export const removeWatchedItem = async (userId: string, masterItemId: number): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabase
|
|
|
|
|
const { error } = await supabaseClient
|
|
|
|
|
.from('user_watched_items')
|
|
|
|
|
.delete()
|
|
|
|
|
.eq('user_id', userId)
|
|
|
|
|
@@ -458,14 +491,13 @@ export const removeWatchedItem = async (userId: string, masterItemId: number): P
|
|
|
|
|
* @returns An array of flyer item objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getFlyerItemsForFlyers = async (flyerIds: number[]): Promise<FlyerItem[]> => {
|
|
|
|
|
if (!supabase || flyerIds.length === 0) return [];
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
if (flyerIds.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.select('*')
|
|
|
|
|
.in('flyer_id', flyerIds);
|
|
|
|
|
if (error) throw new Error(`Error fetching items for flyers: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
.in('flyer_id', flyerIds));
|
|
|
|
|
// Cast the result back to the expected application type
|
|
|
|
|
return (data as unknown as FlyerItem[]) || [];
|
|
|
|
|
};
|
|
|
|
|
@@ -476,9 +508,10 @@ export const getFlyerItemsForFlyers = async (flyerIds: number[]): Promise<FlyerI
|
|
|
|
|
* @returns The total count of items.
|
|
|
|
|
*/
|
|
|
|
|
export const countFlyerItemsForFlyers = async (flyerIds: number[]): Promise<number> => {
|
|
|
|
|
if (!supabase || flyerIds.length === 0) return 0;
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
if (flyerIds.length === 0) return 0;
|
|
|
|
|
|
|
|
|
|
const { count, error } = await supabase
|
|
|
|
|
const { count, error } = await supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.select('*', { count: 'exact', head: true })
|
|
|
|
|
.in('flyer_id', flyerIds);
|
|
|
|
|
@@ -488,24 +521,24 @@ export const countFlyerItemsForFlyers = async (flyerIds: number[]): Promise<numb
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads historical price data for watched items.
|
|
|
|
|
* Fetches all flyer items corresponding to a user's watched items to build a price history.
|
|
|
|
|
* @param watchedItems An array of master grocery items.
|
|
|
|
|
* @returns An array of historical price data points.
|
|
|
|
|
*/
|
|
|
|
|
export const loadAllHistoricalItems = async (watchedItems: MasterGroceryItem[]): Promise<Pick<FlyerItem, 'master_item_id' | 'price_in_cents' | 'created_at'>[]> => {
|
|
|
|
|
if (!supabase || watchedItems.length === 0) return [];
|
|
|
|
|
export const getHistoricalWatchedItems = async (watchedItems: MasterGroceryItem[]): Promise<Pick<FlyerItem, 'master_item_id' | 'price_in_cents' | 'created_at'>[]> => {
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
if (watchedItems.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const watchedItemIds = watchedItems.map(item => item.id);
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('flyer_items')
|
|
|
|
|
.select('master_item_id, price_in_cents, created_at')
|
|
|
|
|
.in('master_item_id', watchedItemIds)
|
|
|
|
|
.not('price_in_cents', 'is', null)
|
|
|
|
|
.order('created_at', { ascending: true });
|
|
|
|
|
if (error) throw new Error(`Error loading historical items: ${error.message}`);
|
|
|
|
|
.order('created_at', { ascending: true }));
|
|
|
|
|
|
|
|
|
|
return data || [];
|
|
|
|
|
return data ?? [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -514,8 +547,9 @@ export const loadAllHistoricalItems = async (watchedItems: MasterGroceryItem[]):
|
|
|
|
|
* @returns The user's profile object.
|
|
|
|
|
*/
|
|
|
|
|
export const getUserProfile = async (userId: string): Promise<Profile | null> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabaseClient
|
|
|
|
|
.from('profiles')
|
|
|
|
|
.select('*, role')
|
|
|
|
|
.eq('id', userId)
|
|
|
|
|
@@ -536,8 +570,8 @@ export const getUserProfile = async (userId: string): Promise<Profile | null> =>
|
|
|
|
|
* @returns The updated profile object.
|
|
|
|
|
*/
|
|
|
|
|
export const updateUserProfile = async (userId: string, updates: { full_name?: string; avatar_url?: string }): Promise<Profile> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
const { data, error } = await supabaseClient
|
|
|
|
|
.from('profiles')
|
|
|
|
|
// Cast to `any` to handle potential JSONB type mismatches if more fields are added
|
|
|
|
|
.update(updates as any)
|
|
|
|
|
@@ -545,7 +579,7 @@ export const updateUserProfile = async (userId: string, updates: { full_name?: s
|
|
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
if (error) throw new Error(`Error updating profile: ${error.message}`);
|
|
|
|
|
// Cast the result back to the expected application type
|
|
|
|
|
if (!data) throw new Error("Update profile did not return data.");
|
|
|
|
|
return data as Profile;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -556,16 +590,15 @@ export const updateUserProfile = async (userId: string, updates: { full_name?: s
|
|
|
|
|
* @returns The updated profile object.
|
|
|
|
|
*/
|
|
|
|
|
export const updateUserPreferences = async (userId: string, preferences: Profile['preferences']): Promise<Profile> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('profiles')
|
|
|
|
|
// Cast to `any` to handle the JSONB type mismatch for `preferences`
|
|
|
|
|
.update({ preferences: preferences as any })
|
|
|
|
|
.eq('id', userId)
|
|
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
if (error) throw new Error(`Error updating preferences: ${error.message}`);
|
|
|
|
|
// Cast the result back to the expected application type
|
|
|
|
|
.single());
|
|
|
|
|
return data as Profile;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -574,8 +607,9 @@ export const updateUserPreferences = async (userId: string, preferences: Profile
|
|
|
|
|
* @param newPassword The new password.
|
|
|
|
|
*/
|
|
|
|
|
export const updateUserPassword = async (newPassword: string): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { error } = await supabase.auth.updateUser({ password: newPassword });
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabaseClient.auth.updateUser({ password: newPassword });
|
|
|
|
|
if (error) throw new Error(`Error updating password: ${error.message}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -585,26 +619,23 @@ export const updateUserPassword = async (newPassword: string): Promise<void> =>
|
|
|
|
|
* @returns An object containing all of the user's data.
|
|
|
|
|
*/
|
|
|
|
|
export const exportUserData = async (userId: string): Promise<object> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data: profile, error: profileError } = await supabase
|
|
|
|
|
const profile = await handleResponse(supabaseClient
|
|
|
|
|
.from('profiles')
|
|
|
|
|
.select('*')
|
|
|
|
|
.eq('id', userId)
|
|
|
|
|
.single();
|
|
|
|
|
if (profileError) throw new Error(`Could not fetch profile: ${profileError.message}`);
|
|
|
|
|
.single());
|
|
|
|
|
|
|
|
|
|
const { data: watchedItems, error: watchedItemsError } = await supabase
|
|
|
|
|
const watchedItems = await handleResponse(supabaseClient
|
|
|
|
|
.from('user_watched_items')
|
|
|
|
|
.select('created_at, item:master_grocery_items(name, category:categories(name))')
|
|
|
|
|
.eq('user_id', userId);
|
|
|
|
|
if (watchedItemsError) throw new Error(`Could not fetch watched items: ${watchedItemsError.message}`);
|
|
|
|
|
.eq('user_id', userId));
|
|
|
|
|
|
|
|
|
|
const { data: shoppingLists, error: shoppingListsError } = await supabase
|
|
|
|
|
const shoppingLists = await handleResponse(supabaseClient
|
|
|
|
|
.from('shopping_lists')
|
|
|
|
|
.select('name, created_at, items:shopping_list_items(custom_item_name, quantity, is_purchased, master_item:master_grocery_items(name))')
|
|
|
|
|
.eq('user_id', userId);
|
|
|
|
|
if (shoppingListsError) throw new Error(`Could not fetch shopping lists: ${shoppingListsError.message}`);
|
|
|
|
|
.eq('user_id', userId));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
profile,
|
|
|
|
|
@@ -621,10 +652,10 @@ export const exportUserData = async (userId: string): Promise<object> => {
|
|
|
|
|
export const deleteUserAccount = async (password: string): Promise<void> => {
|
|
|
|
|
// Adding detailed logging to trace who is calling this function.
|
|
|
|
|
console.trace("deleteUserAccount called. This trace will show the call stack.");
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
// Invoking the 'delete-user' edge function.
|
|
|
|
|
const { data, error } = await supabase.functions.invoke('delete-user', {
|
|
|
|
|
const { data, error } = await supabaseClient.functions.invoke('delete-user', {
|
|
|
|
|
body: { password },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -648,8 +679,9 @@ export const deleteUserAccount = async (password: string): Promise<void> => {
|
|
|
|
|
* @returns The results of the system checks.
|
|
|
|
|
*/
|
|
|
|
|
export const invokeSystemCheckFunction = async (): Promise<any> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase.functions.invoke('system-check');
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabaseClient.functions.invoke('system-check');
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
let errorDetails = `System check function failed: ${error.message}.`;
|
|
|
|
|
@@ -672,8 +704,9 @@ export const invokeSystemCheckFunction = async (): Promise<any> => {
|
|
|
|
|
* Creates the initial development users by invoking a secure Edge Function.
|
|
|
|
|
*/
|
|
|
|
|
export const invokeSeedDatabaseFunction = async (): Promise<{ message: string }> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase.functions.invoke('seed-database');
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabaseClient.functions.invoke('seed-database');
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
let errorDetails = `Edge Function returned a non-2xx status code: ${error.message}.`;
|
|
|
|
|
@@ -698,9 +731,11 @@ export const invokeSeedDatabaseFunction = async (): Promise<{ message: string }>
|
|
|
|
|
* @returns An array of suggested correction objects with related user and item data.
|
|
|
|
|
*/
|
|
|
|
|
export const getSuggestedCorrections = async (): Promise<SuggestedCorrection[]> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
// Re-writing this query explicitly to avoid a complex type-inference issue
|
|
|
|
|
// that was causing the TypeScript language server to report a phantom syntax error.
|
|
|
|
|
const query = supabaseClient
|
|
|
|
|
.from('suggested_corrections')
|
|
|
|
|
.select(`
|
|
|
|
|
*,
|
|
|
|
|
@@ -710,13 +745,22 @@ export const getSuggestedCorrections = async (): Promise<SuggestedCorrection[]>
|
|
|
|
|
.eq('status', 'pending')
|
|
|
|
|
.order('created_at', { ascending: true });
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Failed to fetch suggested corrections: ${error.message}`);
|
|
|
|
|
const { data, error } = await query;
|
|
|
|
|
|
|
|
|
|
return (data || []).map((c: any) => ({
|
|
|
|
|
if (error) {
|
|
|
|
|
throw new Error(`Failed to get suggested corrections: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The data can be null if the query returns no rows, so we handle that case.
|
|
|
|
|
if (!data) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data.map((c: any) => ({
|
|
|
|
|
...c,
|
|
|
|
|
user_email: c.user?.email,
|
|
|
|
|
flyer_item_name: c.item?.item,
|
|
|
|
|
flyer_item_price_display: c.item?.price_display,
|
|
|
|
|
flyer_item_price_display: c.item?.price_display
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -726,9 +770,9 @@ export const getSuggestedCorrections = async (): Promise<SuggestedCorrection[]>
|
|
|
|
|
* @param correctionId The ID of the correction to approve.
|
|
|
|
|
*/
|
|
|
|
|
export const approveCorrection = async (correctionId: number): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabase.rpc('approve_correction', { p_correction_id: correctionId });
|
|
|
|
|
const { error } = await supabaseClient.rpc('approve_correction', { p_correction_id: correctionId });
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
throw new Error(`Failed to approve correction: ${error.message}`);
|
|
|
|
|
@@ -741,9 +785,9 @@ export const approveCorrection = async (correctionId: number): Promise<void> =>
|
|
|
|
|
* @param correctionId The ID of the correction to reject.
|
|
|
|
|
*/
|
|
|
|
|
export const rejectCorrection = async (correctionId: number): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabase.from('suggested_corrections').update({ status: 'rejected', reviewed_at: new Date().toISOString() }).eq('id', correctionId);
|
|
|
|
|
const { error } = await supabaseClient.from('suggested_corrections').update({ status: 'rejected', reviewed_at: new Date().toISOString() }).eq('id', correctionId);
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Failed to reject correction: ${error.message}`);
|
|
|
|
|
};
|
|
|
|
|
@@ -759,8 +803,9 @@ export const rejectCorrection = async (correctionId: number): Promise<void> => {
|
|
|
|
|
* @returns An array of shopping list objects.
|
|
|
|
|
*/
|
|
|
|
|
export const getShoppingLists = async (userId: string): Promise<ShoppingList[]> => {
|
|
|
|
|
if (!supabase) return [];
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
const data = await handleResponse(
|
|
|
|
|
supabaseClient
|
|
|
|
|
.from('shopping_lists')
|
|
|
|
|
.select(`
|
|
|
|
|
*,
|
|
|
|
|
@@ -771,10 +816,9 @@ export const getShoppingLists = async (userId: string): Promise<ShoppingList[]>
|
|
|
|
|
`)
|
|
|
|
|
.eq('user_id', userId)
|
|
|
|
|
.order('created_at', { ascending: true })
|
|
|
|
|
.order('added_at', { ascending: true, referencedTable: 'shopping_list_items' });
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Error fetching shopping lists: ${error.message}`);
|
|
|
|
|
return data || [];
|
|
|
|
|
.order('added_at', { ascending: true, referencedTable: 'shopping_list_items' })
|
|
|
|
|
);
|
|
|
|
|
return data ?? [];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -784,13 +828,12 @@ export const getShoppingLists = async (userId: string): Promise<ShoppingList[]>
|
|
|
|
|
* @returns The newly created shopping list object.
|
|
|
|
|
*/
|
|
|
|
|
export const createShoppingList = async (userId: string, name: string): Promise<ShoppingList> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('shopping_lists')
|
|
|
|
|
.insert({ user_id: userId, name })
|
|
|
|
|
.select()
|
|
|
|
|
.single();
|
|
|
|
|
if (error) throw new Error(`Error creating shopping list: ${error.message}`);
|
|
|
|
|
.single());
|
|
|
|
|
return { ...data, items: [] }; // Return with empty items array
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -799,8 +842,9 @@ export const createShoppingList = async (userId: string, name: string): Promise<
|
|
|
|
|
* @param listId The ID of the list to delete.
|
|
|
|
|
*/
|
|
|
|
|
export const deleteShoppingList = async (listId: number): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabaseClient
|
|
|
|
|
.from('shopping_lists')
|
|
|
|
|
.delete()
|
|
|
|
|
.eq('id', listId);
|
|
|
|
|
@@ -815,25 +859,38 @@ export const deleteShoppingList = async (listId: number): Promise<void> => {
|
|
|
|
|
* @returns The newly created shopping list item.
|
|
|
|
|
*/
|
|
|
|
|
export const addShoppingListItem = async (listId: number, { masterItemId, customItemName }: { masterItemId?: number; customItemName?: string }): Promise<ShoppingListItem> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
if (!masterItemId && !customItemName) throw new Error("Either masterItemId or customItemName must be provided.");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
if (!masterItemId && !customItemName) {
|
|
|
|
|
throw new Error("Either masterItemId or customItemName must be provided.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const itemToInsert = {
|
|
|
|
|
shopping_list_id: listId,
|
|
|
|
|
master_item_id: masterItemId,
|
|
|
|
|
custom_item_name: customItemName,
|
|
|
|
|
quantity: 1
|
|
|
|
|
};
|
|
|
|
|
let query;
|
|
|
|
|
|
|
|
|
|
// Use upsert to handle potential duplicates of master items gracefully
|
|
|
|
|
const query = supabase.from('shopping_list_items').upsert(itemToInsert, {
|
|
|
|
|
onConflict: 'shopping_list_id, master_item_id',
|
|
|
|
|
ignoreDuplicates: masterItemId ? false : true // Only upsert quantity for master items
|
|
|
|
|
}).select('*, master_item:master_grocery_items(name)').single();
|
|
|
|
|
|
|
|
|
|
const { data, error } = await query;
|
|
|
|
|
if (error) throw new Error(`Error adding shopping list item: ${error.message}`);
|
|
|
|
|
return data;
|
|
|
|
|
if (masterItemId) {
|
|
|
|
|
const itemToUpsert = {
|
|
|
|
|
shopping_list_id: listId,
|
|
|
|
|
master_item_id: masterItemId,
|
|
|
|
|
custom_item_name: null, // Ensure the object shape is complete for the type checker
|
|
|
|
|
quantity: 1,
|
|
|
|
|
};
|
|
|
|
|
query = supabaseClient.from('shopping_list_items').upsert(itemToUpsert, { onConflict: 'shopping_list_id, master_item_id' });
|
|
|
|
|
} else {
|
|
|
|
|
const itemToInsert = {
|
|
|
|
|
shopping_list_id: listId,
|
|
|
|
|
master_item_id: null, // Ensure the object shape is complete
|
|
|
|
|
custom_item_name: customItemName,
|
|
|
|
|
quantity: 1,
|
|
|
|
|
};
|
|
|
|
|
query = supabaseClient.from('shopping_list_items').insert(itemToInsert);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bypassing the generic `handleResponse` helper to resolve the complex type inference issue.
|
|
|
|
|
const { data, error } = await query.select('*, master_item:master_grocery_items(name)').single();
|
|
|
|
|
|
|
|
|
|
if (error) throw new Error(`Failed to add shopping list item: ${error.message}`);
|
|
|
|
|
if (!data) throw new Error("Adding shopping list item did not return data.");
|
|
|
|
|
|
|
|
|
|
return data as ShoppingListItem;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -843,17 +900,16 @@ export const addShoppingListItem = async (listId: number, { masterItemId, custom
|
|
|
|
|
* @returns The updated shopping list item.
|
|
|
|
|
*/
|
|
|
|
|
export const updateShoppingListItem = async (itemId: number, updates: Partial<ShoppingListItem>): Promise<ShoppingListItem> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
// The 'id' cannot be part of the update payload.
|
|
|
|
|
const { id, ...updateData } = updates;
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
const { id, master_item, ...updateData } = updates;
|
|
|
|
|
const data = await handleResponse(supabaseClient
|
|
|
|
|
.from('shopping_list_items')
|
|
|
|
|
.update(updateData)
|
|
|
|
|
.eq('id', itemId)
|
|
|
|
|
.select('*, master_item:master_grocery_items(name)')
|
|
|
|
|
.single();
|
|
|
|
|
if (error) throw new Error(`Error updating shopping list item: ${error.message}`);
|
|
|
|
|
return data as ShoppingListItem;
|
|
|
|
|
.single());
|
|
|
|
|
return data;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@@ -861,8 +917,9 @@ export const updateShoppingListItem = async (itemId: number, updates: Partial<Sh
|
|
|
|
|
* @param itemId The ID of the item to remove.
|
|
|
|
|
*/
|
|
|
|
|
export const removeShoppingListItem = async (itemId: number): Promise<void> => {
|
|
|
|
|
if (!supabase) throw new Error("Supabase client not initialized");
|
|
|
|
|
const { error } = await supabase
|
|
|
|
|
const supabaseClient = ensureSupabase();
|
|
|
|
|
|
|
|
|
|
const { error } = await supabaseClient
|
|
|
|
|
.from('shopping_list_items')
|
|
|
|
|
.delete()
|
|
|
|
|
.eq('id', itemId);
|
|
|
|
|
|