From c5abd00da2c420158395a949e5c21edd7cba607c Mon Sep 17 00:00:00 2001 From: Sntrix12 Date: Fri, 27 Feb 2026 01:29:08 +0530 Subject: [PATCH] feat: add return items feature --- README.md | 35 ++++ mun-logi-inventory/SQLCode | 171 ++++++++++++++++++ .../src/components/ItemCard.jsx | 30 ++- .../src/components/ReturnModal.jsx | 73 ++++++++ .../src/pages/CommitteePage.jsx | 35 +++- mun-logi-inventory/src/pages/DispatchLog.jsx | 82 ++++----- 6 files changed, 369 insertions(+), 57 deletions(-) create mode 100644 mun-logi-inventory/SQLCode create mode 100644 mun-logi-inventory/src/components/ReturnModal.jsx diff --git a/README.md b/README.md index 329bdf7..42c4944 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,41 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; +-- Return function (moves items back from a committee to main inventory) +CREATE OR REPLACE FUNCTION return_item( + p_committee_id UUID, + p_item_id UUID, + p_quantity INTEGER +) RETURNS void AS $$ +BEGIN + IF (SELECT quantity FROM committee_inventory + WHERE committee_id = p_committee_id + AND item_id = p_item_id + AND user_id = auth.uid()) < p_quantity THEN + RAISE EXCEPTION 'Committee does not have enough of this item to return'; + END IF; + + UPDATE main_inventory + SET quantity = quantity + p_quantity + WHERE item_id = p_item_id AND user_id = auth.uid(); + + UPDATE committee_inventory + SET quantity = quantity - p_quantity + WHERE committee_id = p_committee_id + AND item_id = p_item_id + AND user_id = auth.uid(); + + INSERT INTO dispatch_log (committee_id, item_id, quantity, item_name, committee_name) + VALUES ( + p_committee_id, + p_item_id, + -p_quantity, + (SELECT name FROM items WHERE id = p_item_id), + (SELECT name FROM committees WHERE id = p_committee_id) + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + -- Account deletion (lets users delete their own account from the app) CREATE OR REPLACE FUNCTION public.delete_own_account() RETURNS void diff --git a/mun-logi-inventory/SQLCode b/mun-logi-inventory/SQLCode new file mode 100644 index 0000000..a66ba3b --- /dev/null +++ b/mun-logi-inventory/SQLCode @@ -0,0 +1,171 @@ +-- Tables (each row is scoped to the signed-in user) +CREATE TABLE items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + name TEXT NOT NULL, + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(name, user_id) +); + +CREATE TABLE main_inventory ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + item_id UUID REFERENCES items(id) ON DELETE CASCADE, + quantity INTEGER NOT NULL DEFAULT 0, + UNIQUE(item_id, user_id) +); + +CREATE TABLE committees ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + name TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(name, user_id) +); + +CREATE TABLE committee_inventory ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + committee_id UUID REFERENCES committees(id) ON DELETE CASCADE, + item_id UUID REFERENCES items(id) ON DELETE CASCADE, + quantity INTEGER NOT NULL DEFAULT 0, + UNIQUE(committee_id, item_id, user_id) +); + +CREATE TABLE dispatch_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID DEFAULT auth.uid(), + committee_id UUID, + item_id UUID, + quantity INTEGER NOT NULL, + dispatched_at TIMESTAMPTZ DEFAULT now(), + note TEXT, + item_name TEXT, + committee_name TEXT +); + +-- Row-level security (users can only access their own data) +ALTER TABLE items ENABLE ROW LEVEL SECURITY; +ALTER TABLE main_inventory ENABLE ROW LEVEL SECURITY; +ALTER TABLE committees ENABLE ROW LEVEL SECURITY; +ALTER TABLE committee_inventory ENABLE ROW LEVEL SECURITY; +ALTER TABLE dispatch_log ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "User owns data" ON items FOR ALL TO authenticated + USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +CREATE POLICY "User owns data" ON main_inventory FOR ALL TO authenticated + USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +CREATE POLICY "User owns data" ON committees FOR ALL TO authenticated + USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +CREATE POLICY "User owns data" ON committee_inventory FOR ALL TO authenticated + USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); +CREATE POLICY "User owns data" ON dispatch_log FOR ALL TO authenticated + USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); + +-- Default inventory created automatically for every new user on sign-up +CREATE OR REPLACE FUNCTION public.create_default_inventory() +RETURNS trigger AS $$ +DECLARE + item_names TEXT[] := ARRAY[ + 'Folder', 'Notepad', 'Pen', 'ID & Lanyard', 'Placard', + 'Sticker', 'Badge', 'Markers', 'Extension Boards', 'Water Bottles' + ]; + item_name TEXT; + new_item_id UUID; +BEGIN + FOREACH item_name IN ARRAY item_names LOOP + INSERT INTO public.items (user_id, name) + VALUES (NEW.id, item_name) RETURNING id INTO new_item_id; + INSERT INTO public.main_inventory (user_id, item_id, quantity) + VALUES (NEW.id, new_item_id, 0); + END LOOP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.create_default_inventory(); + +-- Dispatch function (moves items from main inventory to a committee) +CREATE OR REPLACE FUNCTION dispatch_item( + p_committee_id UUID, + p_item_id UUID, + p_quantity INTEGER +) RETURNS void AS $$ +BEGIN + IF (SELECT quantity FROM main_inventory WHERE item_id = p_item_id AND user_id = auth.uid()) < p_quantity THEN + RAISE EXCEPTION 'Insufficient stock in main inventory'; + END IF; + + UPDATE main_inventory + SET quantity = quantity - p_quantity + WHERE item_id = p_item_id AND user_id = auth.uid(); + + INSERT INTO committee_inventory (committee_id, item_id, quantity) + VALUES (p_committee_id, p_item_id, p_quantity) + ON CONFLICT (committee_id, item_id, user_id) + DO UPDATE SET quantity = committee_inventory.quantity + p_quantity; + + INSERT INTO dispatch_log (committee_id, item_id, quantity, item_name, committee_name) + VALUES ( + p_committee_id, + p_item_id, + p_quantity, + (SELECT name FROM items WHERE id = p_item_id), + (SELECT name FROM committees WHERE id = p_committee_id) + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + +-- Return function (moves items back from a committee to main inventory) +CREATE OR REPLACE FUNCTION return_item( + p_committee_id UUID, + p_item_id UUID, + p_quantity INTEGER +) RETURNS void AS $$ +BEGIN + -- Validate the committee has enough of this item to return + IF (SELECT quantity FROM committee_inventory + WHERE committee_id = p_committee_id + AND item_id = p_item_id + AND user_id = auth.uid()) < p_quantity THEN + RAISE EXCEPTION 'Committee does not have enough of this item to return'; + END IF; + + -- Add back to main inventory + UPDATE main_inventory + SET quantity = quantity + p_quantity + WHERE item_id = p_item_id AND user_id = auth.uid(); + + -- Subtract from committee inventory + UPDATE committee_inventory + SET quantity = quantity - p_quantity + WHERE committee_id = p_committee_id + AND item_id = p_item_id + AND user_id = auth.uid(); + + -- Log the return as a negative-quantity dispatch entry + INSERT INTO dispatch_log (committee_id, item_id, quantity, item_name, committee_name) + VALUES ( + p_committee_id, + p_item_id, + -p_quantity, + (SELECT name FROM items WHERE id = p_item_id), + (SELECT name FROM committees WHERE id = p_committee_id) + ); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + +-- Account deletion (lets users delete their own account from the app) +CREATE OR REPLACE FUNCTION public.delete_own_account() +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = auth, public +AS $$ +BEGIN + DELETE FROM auth.users WHERE id = auth.uid(); +END; +$$; \ No newline at end of file diff --git a/mun-logi-inventory/src/components/ItemCard.jsx b/mun-logi-inventory/src/components/ItemCard.jsx index ce129e1..acc688a 100644 --- a/mun-logi-inventory/src/components/ItemCard.jsx +++ b/mun-logi-inventory/src/components/ItemCard.jsx @@ -1,6 +1,6 @@ import { useState } from 'react' -export default function ItemCard({ item, quantity, onDispatch, onUpdateQuantity, onDelete, showDispatch, showEdit, showDelete, showLowStock = true }) { +export default function ItemCard({ item, quantity, onDispatch, onReturn, onUpdateQuantity, onDelete, showDispatch, showReturn, showEdit, showDelete, showLowStock = true }) { const [editing, setEditing] = useState(false) const [editVal, setEditVal] = useState(quantity) @@ -68,15 +68,25 @@ export default function ItemCard({ item, quantity, onDispatch, onUpdateQuantity, )} - {/* Dispatch button */} - {showDispatch && ( - - )} + {/* Action buttons */} +
+ {showDispatch && ( + + )} + {showReturn && quantity > 0 && ( + + )} +
) } \ No newline at end of file diff --git a/mun-logi-inventory/src/components/ReturnModal.jsx b/mun-logi-inventory/src/components/ReturnModal.jsx new file mode 100644 index 0000000..cdf6232 --- /dev/null +++ b/mun-logi-inventory/src/components/ReturnModal.jsx @@ -0,0 +1,73 @@ +import { useState } from 'react' + +export default function ReturnModal({ itemName, committeeName, maxQuantity, onConfirm, onClose, isLoading, error }) { + const [quantity, setQuantity] = useState(maxQuantity) + + return ( +
+
e.stopPropagation()} + > +

Return Items

+

+ Returning {itemName} from{' '} + {committeeName} back to inventory +

+ + {error && ( +
+ {error} +
+ )} + + {/* Quantity selector */} +
+ + setQuantity(Math.max(1, Math.min(maxQuantity, parseInt(e.target.value) || 1)))} + className="flex-1 bg-gray-50 border border-gray-200 text-gray-800 text-center text-xl font-bold font-montserrat py-2.5 rounded-lg outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-200" + /> + +
+ +

+ Max returnable: {maxQuantity} +

+ +
+ + +
+
+
+ ) +} diff --git a/mun-logi-inventory/src/pages/CommitteePage.jsx b/mun-logi-inventory/src/pages/CommitteePage.jsx index 207cf48..c3066a8 100644 --- a/mun-logi-inventory/src/pages/CommitteePage.jsx +++ b/mun-logi-inventory/src/pages/CommitteePage.jsx @@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { supabase } from '../lib/supabase' import ItemCard from '../components/ItemCard' import DispatchModal from '../components/DispatchModal' +import ReturnModal from '../components/ReturnModal' import ConfirmModal from '../components/ConfirmModal' export default function CommitteePage() { @@ -12,6 +13,7 @@ export default function CommitteePage() { const qc = useQueryClient() const [search, setSearch] = useState('') const [selectedItem, setSelectedItem] = useState(null) + const [returnItem, setReturnItem] = useState(null) const [showDeleteCommittee, setShowDeleteCommittee] = useState(false) const { data: committee } = useQuery({ @@ -68,6 +70,23 @@ export default function CommitteePage() { } }) + const returnMutation = useMutation({ + mutationFn: async ({ itemId, quantity }) => { + const { error } = await supabase.rpc('return_item', { + p_committee_id: committeeId, + p_item_id: itemId, + p_quantity: quantity + }) + if (error) throw error + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['committee_inventory', committeeId] }) + qc.invalidateQueries({ queryKey: ['main_inventory'] }) + qc.invalidateQueries({ queryKey: ['dispatch_log'] }) + setReturnItem(null) + } + }) + const deleteCommittee = useMutation({ mutationFn: async () => { const { error } = await supabase.from('committees').delete().eq('id', committeeId) @@ -99,7 +118,7 @@ export default function CommitteePage() {

{committee?.name}

- Total items dispatched to this committee. Tap a card to dispatch more. + Total items dispatched to this committee. Tap a card to dispatch more, or return items to inventory.