diff --git a/src/app/delivery/history/page.tsx b/src/app/delivery/history/page.tsx new file mode 100644 index 0000000..325b599 --- /dev/null +++ b/src/app/delivery/history/page.tsx @@ -0,0 +1,120 @@ +'use client'; + +import Link from 'next/link'; +import { ArrowLeft, Truck, Clock } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { useDeliveryHistory } from '@/hooks/use-orders'; +import { useAuth } from '@/hooks/use-auth'; + +export default function DeliveryHistoryPage() { + const { deliveryPersonId } = useAuth(); + const { assignments, loading, error } = useDeliveryHistory(deliveryPersonId ?? ''); + + if (!deliveryPersonId || loading) { + return ( +
+
+
+

Loading history...

+
+
+ ); + } + + const now = new Date(); + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const thisWeek = assignments.filter( + (a) => a.delivered_at && new Date(a.delivered_at) >= sevenDaysAgo + ); + const avgPerDay = thisWeek.length > 0 ? (thisWeek.length / 7).toFixed(1) : '0'; + + return ( +
+
+
+
+ + + + +

Delivery History

+
+
+
+ +
+ {error && ( + + + {error} + + + )} + + {/* Stats */} +
+ + +

{assignments.length}

+

Completed

+
+
+ + +

{thisWeek.length}

+

This Week

+
+
+ + +

{avgPerDay}

+

Avg / Day

+
+
+
+ + {assignments.length === 0 ? ( + + + +

No completed deliveries yet.

+
+
+ ) : ( +
+ {assignments.map((assignment) => { + const order = assignment.order; + return ( + + +
+

+ #{order.id.slice(0, 8)} +

+

+ {assignment.delivered_at + ? new Date(assignment.delivered_at).toLocaleString() + : '—'} +

+
+

+ {order.vendor?.name ?? 'Vendor'} + {' → '} + {order.delivery_address} +

+

+ ${Number(order.total_amount).toFixed(2)} +

+
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/src/app/delivery/page.tsx b/src/app/delivery/page.tsx index 2241f3b..cba53e2 100644 --- a/src/app/delivery/page.tsx +++ b/src/app/delivery/page.tsx @@ -1,7 +1,8 @@ 'use client'; import { useState } from 'react'; -import { Bike, Clock, MapPin, Store, Package, Truck, Home, Loader2, Check } from 'lucide-react'; +import Link from 'next/link'; +import { Bike, Clock, MapPin, Store, Package, Truck, Home, Loader2, Check, History } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -60,12 +61,20 @@ export default function DeliveryDashboard() {
-
- -
-

Delivery Dashboard

-

Alex Johnson

+
+
+ +
+

Delivery Dashboard

+

Alex Johnson

+
+ + +
@@ -99,7 +108,7 @@ export default function DeliveryDashboard() { ) : (
- {assignments.map((assignment) => ( + {assignments.filter((a) => a.order).map((assignment) => ( +
+
+

Loading orders...

+
+
+ ); + } + + const activeOrders = orders.filter((o) => ACTIVE_STATUSES.includes(o.status)); + const pastOrders = orders.filter((o) => PAST_STATUSES.includes(o.status)); + + const totalSpent = pastOrders + .filter((o) => o.status === 'delivered') + .reduce((sum, o) => sum + Number(o.total_amount), 0); + + return ( +
+
+
+
+ + + + +

My Orders

+
+
+
+ +
+ {error && ( + + + {error} + + + )} + + {/* Stats */} +
+ + +

{orders.length}

+

Total Orders

+
+
+ + +

${totalSpent.toFixed(2)}

+

Total Spent

+
+
+ + +

{activeOrders.length}

+

Active Orders

+
+
+
+ + {orders.length === 0 ? ( + + + +

No orders yet. Start ordering!

+
+
+ ) : ( +
+ {activeOrders.length > 0 && ( +
+

+ Active +

+
+ {activeOrders.map((order) => ( + + + +
+
+

+ {order.vendor?.name ?? 'Vendor'} +

+

+ {new Date(order.created_at).toLocaleDateString()} · {order.items?.length ?? 0} item{order.items?.length !== 1 ? 's' : ''} +

+
+ +
+

+ ${Number(order.total_amount).toFixed(2)} +

+
+
+ + ))} +
+
+ )} + + {pastOrders.length > 0 && ( +
+

+ Past +

+
+ {pastOrders.map((order) => ( + + +
+
+

+ {order.vendor?.name ?? 'Vendor'} +

+

+ {new Date(order.created_at).toLocaleDateString()} · {order.items?.length ?? 0} item{order.items?.length !== 1 ? 's' : ''} +

+
+ +
+

+ ${Number(order.total_amount).toFixed(2)} +

+
+
+ ))} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/src/app/vendor/orders/page.tsx b/src/app/vendor/orders/page.tsx new file mode 100644 index 0000000..16d6c8a --- /dev/null +++ b/src/app/vendor/orders/page.tsx @@ -0,0 +1,121 @@ +'use client'; + +import Link from 'next/link'; +import { ArrowLeft, ClipboardList, Clock } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { OrderStatusBadge } from '@/components/order-status'; +import { useVendorOrderHistory } from '@/hooks/use-orders'; +import { useAuth } from '@/hooks/use-auth'; + +export default function VendorOrderHistoryPage() { + const { vendorId } = useAuth(); + const { orders, loading, error } = useVendorOrderHistory(vendorId ?? ''); + + if (!vendorId || loading) { + return ( +
+
+
+

Loading history...

+
+
+ ); + } + + const completed = orders.filter((o) => o.status === 'delivered'); + const cancelled = orders.filter((o) => o.status === 'cancelled'); + const totalRevenue = completed.reduce((sum, o) => sum + Number(o.total_amount), 0); + + return ( +
+
+
+
+ + + + +

Order History

+
+
+
+ +
+ {error && ( + + + {error} + + + )} + + {/* Stats */} +
+ + +

{completed.length}

+

Completed

+
+
+ + +

${totalRevenue.toFixed(2)}

+

Total Revenue

+
+
+ + +

{cancelled.length}

+

Cancelled

+
+
+
+ + {orders.length === 0 ? ( + + + +

No completed orders yet.

+
+
+ ) : ( +
+ {orders.map((order) => { + const itemSummary = order.items + ?.map((i) => `${i.quantity}× ${i.name}`) + .join(', '); + return ( + + +
+
+

+ #{order.id.slice(0, 8)} +

+

+ {new Date(order.created_at).toLocaleString()} +

+
+ +
+ {itemSummary && ( +

+ {itemSummary} +

+ )} +

+ ${Number(order.total_amount).toFixed(2)} +

+
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/src/app/vendor/page.tsx b/src/app/vendor/page.tsx index 4733179..c9a90c3 100644 --- a/src/app/vendor/page.tsx +++ b/src/app/vendor/page.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Link from 'next/link'; -import { Store, Clock, ChefHat, Package, Loader2, CheckCircle2, UtensilsCrossed, Settings } from 'lucide-react'; +import { Store, Clock, ChefHat, Package, Loader2, CheckCircle2, UtensilsCrossed, Settings, ClipboardList } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -85,6 +85,12 @@ export default function VendorDashboard() { Menu + + + + +
diff --git a/src/hooks/use-auth.tsx b/src/hooks/use-auth.tsx index ca6d945..cae53e4 100644 --- a/src/hooks/use-auth.tsx +++ b/src/hooks/use-auth.tsx @@ -90,20 +90,20 @@ function ProductionAuthProvider({ }); // Eagerly fetch vendor record regardless of user.roles - const { data: vendor } = await supabase + const { data: vendorRows } = await supabase .from('vendors') .select('id') .eq('user_id', profile.id) - .single(); - setVendorId(vendor?.id || null); + .limit(1); + setVendorId(vendorRows?.[0]?.id || null); // Eagerly fetch delivery person record regardless of user.roles - const { data: dp } = await supabase + const { data: dpRows } = await supabase .from('delivery_persons') .select('id') .eq('user_id', profile.id) - .single(); - setDeliveryPersonId(dp?.id || null); + .limit(1); + setDeliveryPersonId(dpRows?.[0]?.id || null); } }, [supabase] @@ -157,11 +157,22 @@ function ProductionAuthProvider({ }) .select('id') .single() - .then(({ data, error }) => { + .then(async ({ data, error }) => { if (data) { setVendorId(data.id); } else if (error) { - console.error('Failed to provision vendor:', error); + // Insert may have failed because the record already exists — try fetching it + const { data: existingRows } = await supabase + .from('vendors') + .select('id') + .eq('user_id', user.id) + .limit(1); + const existing = existingRows?.[0]; + if (existing) { + setVendorId(existing.id); + } else { + console.error('Failed to provision vendor:', error); + } } provisioningRef.current = null; }); @@ -181,11 +192,22 @@ function ProductionAuthProvider({ }) .select('id') .single() - .then(({ data, error }) => { + .then(async ({ data, error }) => { if (data) { setDeliveryPersonId(data.id); } else if (error) { - console.error('Failed to provision delivery person:', error); + // Insert may have failed because the record already exists — try fetching it + const { data: existingRows } = await supabase + .from('delivery_persons') + .select('id') + .eq('user_id', user.id) + .limit(1); + const existing = existingRows?.[0]; + if (existing) { + setDeliveryPersonId(existing.id); + } else { + console.error('Failed to provision delivery person:', error); + } } provisioningRef.current = null; }); @@ -211,24 +233,12 @@ function ProductionAuthProvider({ const { data, error } = await supabase.auth.signUp({ email, password, + options: { data: { name } }, }); if (error) return { error: error.message }; if (!data.user) return { error: 'Sign up failed' }; - // All new users start as customer - const { error: profileError } = await supabase.from('users').insert({ - id: data.user.id, - email, - name, - phone: '', - roles: ['customer'], - default_role: 'customer', - }); - - if (profileError) { - return { error: 'Account created but profile setup failed. Please contact support.' }; - } - + // Profile is created automatically via the handle_new_user DB trigger. return { error: null }; }, [supabase] diff --git a/src/hooks/use-orders.tsx b/src/hooks/use-orders.tsx index ffa0db8..8b4649f 100644 --- a/src/hooks/use-orders.tsx +++ b/src/hooks/use-orders.tsx @@ -212,6 +212,103 @@ export function useDeliveryAssignments(deliveryPersonId: string) { return { assignments, loading, error, refresh }; } +// Hook for customer order history (all orders, newest first) +export function useCustomerOrders(customerId: string) { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!customerId) { + setLoading(false); + return; + } + const supabase = createClient(); + supabase + .from('orders') + .select(` + *, + vendor:vendors(*), + items:order_items(*) + `) + .eq('customer_id', customerId) + .order('created_at', { ascending: false }) + .then(({ data, error: fetchError }) => { + if (fetchError) setError(fetchError.message); + else setOrders(data as unknown as OrderWithDetails[]); + setLoading(false); + }); + }, [customerId]); + + return { orders, loading, error }; +} + +// Hook for vendor order history (completed + cancelled only) +export function useVendorOrderHistory(vendorId: string) { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!vendorId) { + setLoading(false); + return; + } + const supabase = createClient(); + supabase + .from('orders') + .select(` + *, + items:order_items(*) + `) + .eq('vendor_id', vendorId) + .in('status', ['delivered', 'cancelled']) + .order('created_at', { ascending: false }) + .then(({ data, error: fetchError }) => { + if (fetchError) setError(fetchError.message); + else setOrders(data as unknown as OrderWithDetails[]); + setLoading(false); + }); + }, [vendorId]); + + return { orders, loading, error }; +} + +// Hook for delivery person history (delivered assignments only) +export function useDeliveryHistory(deliveryPersonId: string) { + const [assignments, setAssignments] = useState<(DeliveryAssignment & { order: OrderWithDetails })[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!deliveryPersonId) { + setLoading(false); + return; + } + const supabase = createClient(); + supabase + .from('delivery_assignments') + .select(` + *, + order:orders( + *, + vendor:vendors(*), + items:order_items(*) + ) + `) + .eq('delivery_person_id', deliveryPersonId) + .eq('status', 'delivered') + .order('delivered_at', { ascending: false }) + .then(({ data, error: fetchError }) => { + if (fetchError) setError(fetchError.message); + else setAssignments(data as unknown as (DeliveryAssignment & { order: OrderWithDetails })[]); + setLoading(false); + }); + }, [deliveryPersonId]); + + return { assignments, loading, error }; +} + // Update order status export async function updateOrderStatus(orderId: string, status: Order['status']) { const response = await fetch(`/api/orders/${orderId}/status`, { diff --git a/supabase/full_reset.sql b/supabase/full_reset.sql new file mode 100644 index 0000000..2b0528c --- /dev/null +++ b/supabase/full_reset.sql @@ -0,0 +1,379 @@ +-- ============================================================ +-- Project Firefly — Full Reset +-- Drops everything and rebuilds schema, policies, and seed data. +-- Run this in Supabase SQL Editor. +-- +-- Test accounts after running: +-- customer@test.firefly / test1234 +-- vendor@test.firefly / test1234 +-- delivery@test.firefly / test1234 +-- ============================================================ + + +-- ------------------------------------------------------------ +-- PART 1: DROP EVERYTHING +-- ------------------------------------------------------------ + +-- Drop triggers +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +DROP TRIGGER IF EXISTS update_users_updated_at ON public.users; +DROP TRIGGER IF EXISTS update_orders_updated_at ON public.orders; + +-- Drop functions +DROP FUNCTION IF EXISTS public.handle_new_user(); +DROP FUNCTION IF EXISTS public.delivery_person_has_assignment(UUID); +DROP FUNCTION IF EXISTS public.update_updated_at(); + +-- Drop tables (cascade handles foreign keys) +DROP TABLE IF EXISTS public.delivery_assignments CASCADE; +DROP TABLE IF EXISTS public.order_items CASCADE; +DROP TABLE IF EXISTS public.orders CASCADE; +DROP TABLE IF EXISTS public.menu_items CASCADE; +DROP TABLE IF EXISTS public.delivery_persons CASCADE; +DROP TABLE IF EXISTS public.vendors CASCADE; +DROP TABLE IF EXISTS public.users CASCADE; + +-- Remove test auth users +DELETE FROM auth.users WHERE email IN ( + 'customer@test.firefly', + 'vendor@test.firefly', + 'delivery@test.firefly' +); + + +-- ------------------------------------------------------------ +-- PART 2: SCHEMA +-- ------------------------------------------------------------ + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Users +CREATE TABLE public.users ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + email TEXT NOT NULL, + name TEXT NOT NULL, + phone TEXT, + avatar_url TEXT, + roles TEXT[] DEFAULT ARRAY['customer']::TEXT[], + default_role TEXT DEFAULT 'customer', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Vendors +CREATE TABLE public.vendors ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES public.users(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + logo_url TEXT, + address TEXT, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT vendors_user_id_unique UNIQUE (user_id) +); + +-- Menu items +CREATE TABLE public.menu_items ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + vendor_id UUID NOT NULL REFERENCES public.vendors(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + price DECIMAL(10,2) NOT NULL, + image_url TEXT, + category TEXT, + is_available BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Delivery persons +CREATE TABLE public.delivery_persons ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES public.users(id) ON DELETE CASCADE, + is_active BOOLEAN DEFAULT true, + is_available BOOLEAN DEFAULT true, + vehicle_type TEXT DEFAULT 'bike', + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT delivery_persons_user_id_unique UNIQUE (user_id) +); + +-- Orders +CREATE TABLE public.orders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + customer_id UUID NOT NULL REFERENCES public.users(id), + vendor_id UUID NOT NULL REFERENCES public.vendors(id), + delivery_person_id UUID REFERENCES public.delivery_persons(id), + status TEXT NOT NULL DEFAULT 'pending', + vendor_accepted BOOLEAN DEFAULT false, + total_amount DECIMAL(10,2) NOT NULL, + delivery_address TEXT NOT NULL, + delivery_notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Order items +CREATE TABLE public.order_items ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + order_id UUID NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE, + menu_item_id UUID NOT NULL REFERENCES public.menu_items(id), + name TEXT NOT NULL, + quantity INTEGER NOT NULL DEFAULT 1, + unit_price DECIMAL(10,2) NOT NULL, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Delivery assignments +CREATE TABLE public.delivery_assignments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + order_id UUID NOT NULL REFERENCES public.orders(id) ON DELETE CASCADE, + delivery_person_id UUID NOT NULL REFERENCES public.delivery_persons(id), + status TEXT NOT NULL DEFAULT 'pending', + assigned_at TIMESTAMPTZ DEFAULT NOW(), + accepted_at TIMESTAMPTZ, + picked_up_at TIMESTAMPTZ, + delivered_at TIMESTAMPTZ +); + +-- Indexes +CREATE INDEX idx_orders_customer ON public.orders(customer_id); +CREATE INDEX idx_orders_vendor ON public.orders(vendor_id); +CREATE INDEX idx_orders_status ON public.orders(status); +CREATE INDEX idx_menu_items_vendor ON public.menu_items(vendor_id); +CREATE INDEX idx_delivery_assignments_order ON public.delivery_assignments(order_id); +CREATE INDEX idx_delivery_assignments_person ON public.delivery_assignments(delivery_person_id); + + +-- ------------------------------------------------------------ +-- PART 3: FUNCTIONS & TRIGGERS +-- ------------------------------------------------------------ + +-- updated_at trigger function +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON public.users + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER update_orders_updated_at + BEFORE UPDATE ON public.orders + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- Auto-create user profile on signup (bypasses RLS) +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + INSERT INTO public.users (id, email, name, phone, roles, default_role) + VALUES ( + NEW.id, + NEW.email, + COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)), + '', + ARRAY['customer'], + 'customer' + ) + ON CONFLICT (id) DO NOTHING; + RETURN NEW; +END; +$$; + +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + +-- SECURITY DEFINER function to check delivery assignments without recursion +CREATE OR REPLACE FUNCTION public.delivery_person_has_assignment(order_uuid UUID) +RETURNS BOOLEAN +LANGUAGE sql +SECURITY DEFINER +STABLE +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.delivery_assignments da + JOIN public.delivery_persons dp ON dp.id = da.delivery_person_id + WHERE da.order_id = order_uuid + AND dp.user_id = auth.uid() + ); +$$; + + +-- ------------------------------------------------------------ +-- PART 4: ROW LEVEL SECURITY +-- ------------------------------------------------------------ + +ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.vendors ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.menu_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.order_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.delivery_persons ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.delivery_assignments ENABLE ROW LEVEL SECURITY; + +-- users +CREATE POLICY "Users are viewable by everyone" ON public.users FOR SELECT USING (true); +CREATE POLICY "Users can insert own record" ON public.users FOR INSERT WITH CHECK (auth.uid() = id); +CREATE POLICY "Users can update own record" ON public.users FOR UPDATE USING (auth.uid() = id); + +-- vendors +CREATE POLICY "Vendors are viewable by everyone" ON public.vendors FOR SELECT USING (true); +CREATE POLICY "Users can insert own vendor" ON public.vendors FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Vendors can update own record" ON public.vendors FOR UPDATE USING (auth.uid() = user_id); + +-- menu_items +CREATE POLICY "Menu items are viewable by everyone" ON public.menu_items FOR SELECT USING (true); +CREATE POLICY "Vendors can insert own menu items" ON public.menu_items FOR INSERT WITH CHECK ( + vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) +); +CREATE POLICY "Vendors can update own menu items" ON public.menu_items FOR UPDATE USING ( + vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) +); +CREATE POLICY "Vendors can delete own menu items" ON public.menu_items FOR DELETE USING ( + vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) +); + +-- delivery_persons +CREATE POLICY "Delivery persons viewable by everyone" ON public.delivery_persons FOR SELECT USING (true); +CREATE POLICY "Users can create own delivery person" ON public.delivery_persons FOR INSERT WITH CHECK (auth.uid() = user_id); + +-- orders +CREATE POLICY "Customers can view own orders" ON public.orders FOR SELECT USING (auth.uid() = customer_id); +CREATE POLICY "Vendors can view their orders" ON public.orders FOR SELECT USING ( + vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) +); +CREATE POLICY "Delivery can view assigned orders" ON public.orders FOR SELECT USING ( + delivery_person_id IN (SELECT id FROM public.delivery_persons WHERE user_id = auth.uid()) + OR public.delivery_person_has_assignment(id) +); +CREATE POLICY "Customers can create orders" ON public.orders FOR INSERT WITH CHECK (auth.uid() = customer_id); +CREATE POLICY "Customers can update own orders" ON public.orders FOR UPDATE USING (auth.uid() = customer_id); +CREATE POLICY "Vendors can update their orders" ON public.orders FOR UPDATE USING ( + vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) +); +CREATE POLICY "Delivery can update assigned orders" ON public.orders FOR UPDATE USING ( + delivery_person_id IN (SELECT id FROM public.delivery_persons WHERE user_id = auth.uid()) + OR public.delivery_person_has_assignment(id) +); + +-- order_items +CREATE POLICY "Order items viewable with order access" ON public.order_items FOR SELECT USING ( + order_id IN ( + SELECT id FROM public.orders + WHERE customer_id = auth.uid() + OR vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) + OR delivery_person_id IN (SELECT id FROM public.delivery_persons WHERE user_id = auth.uid()) + ) +); +CREATE POLICY "Customers can create order items" ON public.order_items FOR INSERT WITH CHECK ( + order_id IN (SELECT id FROM public.orders WHERE customer_id = auth.uid()) +); + +-- delivery_assignments +CREATE POLICY "Delivery can view own assignments" ON public.delivery_assignments FOR SELECT USING ( + delivery_person_id IN (SELECT id FROM public.delivery_persons WHERE user_id = auth.uid()) +); +CREATE POLICY "Vendors can view order assignments" ON public.delivery_assignments FOR SELECT USING ( + order_id IN ( + SELECT id FROM public.orders + WHERE vendor_id IN (SELECT id FROM public.vendors WHERE user_id = auth.uid()) + ) +); +CREATE POLICY "System can create assignments" ON public.delivery_assignments FOR INSERT WITH CHECK (true); +CREATE POLICY "Delivery can update own assignments" ON public.delivery_assignments FOR UPDATE USING ( + delivery_person_id IN (SELECT id FROM public.delivery_persons WHERE user_id = auth.uid()) +); + +-- Realtime +ALTER PUBLICATION supabase_realtime ADD TABLE public.orders; +ALTER PUBLICATION supabase_realtime ADD TABLE public.delivery_assignments; + + +-- ------------------------------------------------------------ +-- PART 5: CREATE TEST AUTH USERS +-- ------------------------------------------------------------ + +DO $$ +DECLARE + customer_id UUID := '00000000-0000-0000-0000-000000000001'; + vendor_id UUID := '00000000-0000-0000-0000-000000000002'; + delivery_id UUID := '00000000-0000-0000-0000-000000000003'; +BEGIN + INSERT INTO auth.users ( + id, instance_id, aud, role, email, + encrypted_password, email_confirmed_at, + created_at, updated_at, + raw_user_meta_data, raw_app_meta_data, + is_super_admin, confirmation_token, recovery_token, + email_change_token_new, email_change + ) VALUES + ( + customer_id, '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', 'customer@test.firefly', + crypt('test1234', gen_salt('bf')), NOW(), NOW(), NOW(), + '{"name":"Test Customer"}'::jsonb, + '{"provider":"email","providers":["email"]}'::jsonb, + false, '', '', '', '' + ), + ( + vendor_id, '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', 'vendor@test.firefly', + crypt('test1234', gen_salt('bf')), NOW(), NOW(), NOW(), + '{"name":"Maria Garcia"}'::jsonb, + '{"provider":"email","providers":["email"]}'::jsonb, + false, '', '', '', '' + ), + ( + delivery_id, '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', 'delivery@test.firefly', + crypt('test1234', gen_salt('bf')), NOW(), NOW(), NOW(), + '{"name":"Alex Johnson"}'::jsonb, + '{"provider":"email","providers":["email"]}'::jsonb, + false, '', '', '', '' + ); +END $$; + + +-- ------------------------------------------------------------ +-- PART 6: SEED DATA +-- ------------------------------------------------------------ + +INSERT INTO public.users (id, email, name, phone, roles, default_role) VALUES + ('00000000-0000-0000-0000-000000000001', 'customer@test.firefly', 'Test Customer', '555-0101', ARRAY['customer'], 'customer'), + ('00000000-0000-0000-0000-000000000002', 'vendor@test.firefly', 'Maria Garcia', '555-0102', ARRAY['vendor'], 'vendor'), + ('00000000-0000-0000-0000-000000000003', 'delivery@test.firefly', 'Alex Johnson', '555-0103', ARRAY['delivery'], 'delivery') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.vendors (id, user_id, name, description, address, is_active) VALUES + ('10000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002', + 'Maria''s Kitchen', 'Authentic homemade Mexican food prepared with love.', '123 Main Street, Downtown', true) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.menu_items (id, vendor_id, name, description, price, category, is_available) VALUES + ('20000000-0000-0000-0000-000000000001', '10000000-0000-0000-0000-000000000001', 'Chicken Tacos', 'Three soft corn tortillas with seasoned chicken, fresh cilantro, onions, and salsa verde', 12.99, 'Tacos', true), + ('20000000-0000-0000-0000-000000000002', '10000000-0000-0000-0000-000000000001', 'Beef Burrito', 'Large flour tortilla stuffed with seasoned ground beef, rice, beans, cheese, and pico de gallo', 14.99, 'Burritos', true), + ('20000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001', 'Veggie Quesadilla', 'Grilled flour tortilla with melted cheese, peppers, onions, and mushrooms. Served with sour cream', 10.99, 'Quesadillas', true), + ('20000000-0000-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001', 'Chips & Guacamole', 'Fresh house-made guacamole with crispy tortilla chips', 7.99, 'Sides', true), + ('20000000-0000-0000-0000-000000000005', '10000000-0000-0000-0000-000000000001', 'Horchata', 'Traditional Mexican rice drink with cinnamon (16oz)', 3.99, 'Drinks', true) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.delivery_persons (id, user_id, is_active, is_available, vehicle_type) VALUES + ('30000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', true, true, 'bike') +ON CONFLICT (id) DO NOTHING; + +-- ============================================================ +-- Done. +-- customer@test.firefly / test1234 +-- vendor@test.firefly / test1234 +-- delivery@test.firefly / test1234 +-- ============================================================ diff --git a/supabase/migrations/007_delivery_persons_unique.sql b/supabase/migrations/007_delivery_persons_unique.sql new file mode 100644 index 0000000..ad0d70a --- /dev/null +++ b/supabase/migrations/007_delivery_persons_unique.sql @@ -0,0 +1,45 @@ +-- Ensure each user can only have one delivery_person record +-- Before adding constraint, remove duplicates by keeping the oldest per user_id +DELETE FROM public.delivery_assignments +WHERE delivery_person_id IN ( + SELECT id FROM public.delivery_persons + WHERE id NOT IN ( + SELECT DISTINCT ON (user_id) id + FROM public.delivery_persons + ORDER BY user_id, created_at ASC + ) +); + +DELETE FROM public.delivery_persons +WHERE id NOT IN ( + SELECT DISTINCT ON (user_id) id + FROM public.delivery_persons + ORDER BY user_id, created_at ASC +); + +ALTER TABLE public.delivery_persons + ADD CONSTRAINT delivery_persons_user_id_unique UNIQUE (user_id); + +-- Re-ensure INSERT policies exist (idempotent via DO block) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'delivery_persons' + AND policyname = 'Users can create own delivery person' + ) THEN + CREATE POLICY "Users can create own delivery person" ON public.delivery_persons + FOR INSERT WITH CHECK (auth.uid() = user_id); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'vendors' + AND policyname = 'Users can insert own vendor' + ) THEN + CREATE POLICY "Users can insert own vendor" ON public.vendors + FOR INSERT WITH CHECK (auth.uid() = user_id); + END IF; +END $$; diff --git a/supabase/migrations/008_fix_order_delivery_rls.sql b/supabase/migrations/008_fix_order_delivery_rls.sql new file mode 100644 index 0000000..ebb0782 --- /dev/null +++ b/supabase/migrations/008_fix_order_delivery_rls.sql @@ -0,0 +1,20 @@ +-- Allow the server (running as customer) to update their own orders +-- This is needed so delivery_person_id gets set when an order is placed +CREATE POLICY "Customers can update own orders" ON public.orders + FOR UPDATE USING (auth.uid() = customer_id); + +-- Extend delivery order visibility to also cover orders linked via delivery_assignments +-- Handles the case where orders.delivery_person_id wasn't set due to the missing policy above +DROP POLICY IF EXISTS "Delivery can view assigned orders" ON public.orders; +CREATE POLICY "Delivery can view assigned orders" ON public.orders + FOR SELECT USING ( + delivery_person_id IN ( + SELECT id FROM public.delivery_persons WHERE user_id = auth.uid() + ) + OR id IN ( + SELECT order_id FROM public.delivery_assignments + WHERE delivery_person_id IN ( + SELECT id FROM public.delivery_persons WHERE user_id = auth.uid() + ) + ) + ); diff --git a/supabase/migrations/009_fix_orders_policy_recursion.sql b/supabase/migrations/009_fix_orders_policy_recursion.sql new file mode 100644 index 0000000..0db4d11 --- /dev/null +++ b/supabase/migrations/009_fix_orders_policy_recursion.sql @@ -0,0 +1,30 @@ +-- Fix infinite recursion: delivery orders policy referenced delivery_assignments +-- which has a foreign key back to orders, causing a loop. +-- Use a SECURITY DEFINER function to break the cycle. + +DROP POLICY IF EXISTS "Delivery can view assigned orders" ON public.orders; + +-- This function runs as DB owner (bypasses RLS), so it can safely query +-- delivery_assignments without re-triggering the orders policy. +CREATE OR REPLACE FUNCTION public.delivery_person_has_assignment(order_uuid UUID) +RETURNS BOOLEAN +LANGUAGE sql +SECURITY DEFINER +STABLE +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.delivery_assignments da + JOIN public.delivery_persons dp ON dp.id = da.delivery_person_id + WHERE da.order_id = order_uuid + AND dp.user_id = auth.uid() + ); +$$; + +CREATE POLICY "Delivery can view assigned orders" ON public.orders + FOR SELECT USING ( + delivery_person_id IN ( + SELECT id FROM public.delivery_persons WHERE user_id = auth.uid() + ) + OR public.delivery_person_has_assignment(id) + ); diff --git a/supabase/migrations/010_fix_delivery_update_policy.sql b/supabase/migrations/010_fix_delivery_update_policy.sql new file mode 100644 index 0000000..2e6fdd3 --- /dev/null +++ b/supabase/migrations/010_fix_delivery_update_policy.sql @@ -0,0 +1,18 @@ +-- Fix existing orders that are missing delivery_person_id +-- (Orders placed before the customer UPDATE policy was added) +UPDATE public.orders o +SET delivery_person_id = da.delivery_person_id +FROM public.delivery_assignments da +WHERE da.order_id = o.id + AND o.delivery_person_id IS NULL; + +-- Extend delivery UPDATE policy to also match via delivery_assignments +-- Uses the same SECURITY DEFINER function from migration 009 to avoid recursion +DROP POLICY IF EXISTS "Delivery can update assigned orders" ON public.orders; +CREATE POLICY "Delivery can update assigned orders" ON public.orders + FOR UPDATE USING ( + delivery_person_id IN ( + SELECT id FROM public.delivery_persons WHERE user_id = auth.uid() + ) + OR public.delivery_person_has_assignment(id) + ); diff --git a/supabase/migrations/011_auto_create_user_profile.sql b/supabase/migrations/011_auto_create_user_profile.sql new file mode 100644 index 0000000..c384667 --- /dev/null +++ b/supabase/migrations/011_auto_create_user_profile.sql @@ -0,0 +1,29 @@ +-- Auto-create public.users profile when a new auth user is created. +-- This runs as the DB owner (SECURITY DEFINER), bypassing RLS, so it works +-- regardless of whether the client has an active session (e.g. email confirmation flows). + +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + INSERT INTO public.users (id, email, name, phone, roles, default_role) + VALUES ( + NEW.id, + NEW.email, + COALESCE(NEW.raw_user_meta_data->>'name', split_part(NEW.email, '@', 1)), + '', + ARRAY['customer'], + 'customer' + ) + ON CONFLICT (id) DO NOTHING; + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); diff --git a/supabase/reset_and_seed.sql b/supabase/reset_and_seed.sql new file mode 100644 index 0000000..5a9add6 --- /dev/null +++ b/supabase/reset_and_seed.sql @@ -0,0 +1,149 @@ +-- ============================================================ +-- Project Firefly — Reset & Seed +-- Run this in Supabase SQL Editor to wipe all data and create +-- three ready-to-use test accounts. +-- +-- Credentials after running: +-- customer@test.firefly / test1234 (customer role) +-- vendor@test.firefly / test1234 (vendor role) +-- delivery@test.firefly / test1234 (delivery role) +-- ============================================================ + +-- ------------------------------------------------------------ +-- 1. WIPE all app data (preserves schema & policies) +-- ------------------------------------------------------------ +DELETE FROM public.delivery_assignments; +DELETE FROM public.order_items; +DELETE FROM public.orders; +DELETE FROM public.menu_items; +DELETE FROM public.delivery_persons; +DELETE FROM public.vendors; +DELETE FROM public.users; + +-- Remove existing test auth users (ignore errors if they don't exist) +DELETE FROM auth.users WHERE email IN ( + 'customer@test.firefly', + 'vendor@test.firefly', + 'delivery@test.firefly' +); + +-- ------------------------------------------------------------ +-- 2. CREATE auth users +-- Using Supabase's built-in function so passwords are hashed +-- correctly and sessions work immediately (no email confirm needed). +-- ------------------------------------------------------------ +SELECT extensions.uuid_generate_v4(); -- ensure uuid extension is loaded + +DO $$ +DECLARE + customer_id UUID := '00000000-0000-0000-0000-000000000001'; + vendor_id UUID := '00000000-0000-0000-0000-000000000002'; + delivery_id UUID := '00000000-0000-0000-0000-000000000003'; +BEGIN + -- Customer + INSERT INTO auth.users ( + id, instance_id, aud, role, email, + encrypted_password, email_confirmed_at, + created_at, updated_at, + raw_user_meta_data, raw_app_meta_data, + is_super_admin, confirmation_token, recovery_token, + email_change_token_new, email_change + ) VALUES ( + customer_id, + '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', + 'customer@test.firefly', + crypt('test1234', gen_salt('bf')), + NOW(), NOW(), NOW(), + '{"name": "Test Customer"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + false, '', '', '', '' + ); + + -- Vendor + INSERT INTO auth.users ( + id, instance_id, aud, role, email, + encrypted_password, email_confirmed_at, + created_at, updated_at, + raw_user_meta_data, raw_app_meta_data, + is_super_admin, confirmation_token, recovery_token, + email_change_token_new, email_change + ) VALUES ( + vendor_id, + '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', + 'vendor@test.firefly', + crypt('test1234', gen_salt('bf')), + NOW(), NOW(), NOW(), + '{"name": "Maria Garcia"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + false, '', '', '', '' + ); + + -- Delivery + INSERT INTO auth.users ( + id, instance_id, aud, role, email, + encrypted_password, email_confirmed_at, + created_at, updated_at, + raw_user_meta_data, raw_app_meta_data, + is_super_admin, confirmation_token, recovery_token, + email_change_token_new, email_change + ) VALUES ( + delivery_id, + '00000000-0000-0000-0000-000000000000', + 'authenticated', 'authenticated', + 'delivery@test.firefly', + crypt('test1234', gen_salt('bf')), + NOW(), NOW(), NOW(), + '{"name": "Alex Johnson"}'::jsonb, + '{"provider": "email", "providers": ["email"]}'::jsonb, + false, '', '', '', '' + ); +END $$; + +-- ------------------------------------------------------------ +-- 3. SEED public.users profiles +-- ------------------------------------------------------------ +INSERT INTO public.users (id, email, name, phone, roles, default_role) VALUES + ('00000000-0000-0000-0000-000000000001', 'customer@test.firefly', 'Test Customer', '555-0101', ARRAY['customer'], 'customer'), + ('00000000-0000-0000-0000-000000000002', 'vendor@test.firefly', 'Maria Garcia', '555-0102', ARRAY['vendor'], 'vendor'), + ('00000000-0000-0000-0000-000000000003', 'delivery@test.firefly', 'Alex Johnson', '555-0103', ARRAY['delivery'], 'delivery') +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- 4. SEED vendor & menu +-- ------------------------------------------------------------ +INSERT INTO public.vendors (id, user_id, name, description, address, is_active) +VALUES ( + '10000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000002', + 'Maria''s Kitchen', + 'Authentic homemade Mexican food prepared with love.', + '123 Main Street, Downtown', + true +) ON CONFLICT (id) DO NOTHING; + +INSERT INTO public.menu_items (id, vendor_id, name, description, price, category, is_available) VALUES + ('20000000-0000-0000-0000-000000000001', '10000000-0000-0000-0000-000000000001', 'Chicken Tacos', 'Three soft corn tortillas with seasoned chicken, fresh cilantro, onions, and salsa verde', 12.99, 'Tacos', true), + ('20000000-0000-0000-0000-000000000002', '10000000-0000-0000-0000-000000000001', 'Beef Burrito', 'Large flour tortilla stuffed with seasoned ground beef, rice, beans, cheese, and pico de gallo', 14.99, 'Burritos', true), + ('20000000-0000-0000-0000-000000000003', '10000000-0000-0000-0000-000000000001', 'Veggie Quesadilla', 'Grilled flour tortilla with melted cheese, peppers, onions, and mushrooms. Served with sour cream', 10.99, 'Quesadillas', true), + ('20000000-0000-0000-0000-000000000004', '10000000-0000-0000-0000-000000000001', 'Chips & Guacamole', 'Fresh house-made guacamole with crispy tortilla chips', 7.99, 'Sides', true), + ('20000000-0000-0000-0000-000000000005', '10000000-0000-0000-0000-000000000001', 'Horchata', 'Traditional Mexican rice drink with cinnamon (16oz)', 3.99, 'Drinks', true) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- 5. SEED delivery person profile +-- ------------------------------------------------------------ +INSERT INTO public.delivery_persons (id, user_id, is_active, is_available, vehicle_type) +VALUES ( + '30000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000003', + true, true, 'bike' +) ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Done. Test accounts: +-- customer@test.firefly / test1234 +-- vendor@test.firefly / test1234 +-- delivery@test.firefly / test1234 +-- ------------------------------------------------------------