feat: Shopping List UI + Roadmap Update

📦 Shopping List Feature (Phase 3A)
- Full SupplyItem model with Vendor & ProductUrl
- Shopping List / Inventory toggle
- Add Item Modal (Category, Thresholds, Vendor info)
- 'Order Now' external link logic
- 'Mark Ordered' tracking
- Quantity +/- adjustments

📅 Roadmap Update:
- Defined Unified Master Calendar (Phase 4)
- Added granular toggles (Emp Schedules, Taxes, Compliance, Plant Cycle, etc.)

🚀 Status:
- Backend deployed (Schema synced)
- Frontend deploying...
This commit is contained in:
fullsizemalt 2025-12-09 15:26:35 -08:00
parent 22574359ba
commit f95b626724
7 changed files with 566 additions and 33 deletions

View file

@ -224,20 +224,27 @@
### Features
- Task templates (recurring tasks)
- Task scheduling (calendar view)
- Task assignment (by user/role)
- Task completion tracking
- **Unified Master Calendar**:
- Toggles for:
- Employee Schedules (Rooster)
- Vacation Requests
- Shift/Hours Logs
- Vendor Deliveries/Meetings
- Plant Cycle (Veg/Flower/Harvest dates)
- Tax Schedule (Deadlines)
- Compliance Matters (Inspections, renewals)
- Task templating & assignment
- Automatic stock deduction
- Batch lifecycle tasks
- Batch lifecycle integration
- Notifications and reminders
**Database**:
- CalendarEvent model
- EventType enum (SCHEDULE, VACATION, VENDOR, PLANT, TAX, COMPLIANCE)
- Task model
- TaskTemplate model
- TaskAssignment model
- TaskStatus enum
**Spec**: `specs/tasks-and-scheduling.md`

View file

@ -250,6 +250,8 @@ model SupplyItem {
minThreshold Int @default(0)
unit String // "each", "box", "roll", "gallon", etc.
location String? // "Storage Room", "Bathroom", etc.
vendor String? // "Amazon", "Local", etc.
productUrl String? // Link to reorder
lastOrdered DateTime?
notes String?

View file

@ -69,13 +69,15 @@ export async function createSupplyItem(
minThreshold?: number;
unit: string;
location?: string;
vendor?: string;
productUrl?: string;
notes?: string;
};
}>,
reply: FastifyReply
) {
try {
const { name, category, quantity, minThreshold, unit, location, notes } = request.body;
const { name, category, quantity, minThreshold, unit, location, vendor, productUrl, notes } = request.body;
const item = await prisma.supplyItem.create({
data: {
@ -85,6 +87,8 @@ export async function createSupplyItem(
minThreshold: minThreshold || 0,
unit,
location,
vendor,
productUrl,
notes,
},
});
@ -106,6 +110,8 @@ export async function updateSupplyItem(
minThreshold?: number;
unit?: string;
location?: string;
vendor?: string;
productUrl?: string;
notes?: string;
};
}>,

View file

@ -10,19 +10,24 @@ import {
Clock,
LogOut,
Menu,
X
X,
User,
ChevronDown,
Package
} from 'lucide-react';
export default function Layout() {
const { user, logout } = useAuth();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const [userMenuOpen, setUserMenuOpen] = React.useState(false);
const navItems = [
{ label: 'Dashboard', path: '/', icon: LayoutDashboard },
{ label: 'Walkthrough', path: '/walkthrough', icon: CheckSquare },
{ label: 'Rooms', path: '/rooms', icon: Home },
{ label: 'Batches', path: '/batches', icon: Sprout },
{ label: 'Inventory', path: '/supplies', icon: Package },
{ label: 'Time', path: '/timeclock', icon: Clock },
];
@ -119,8 +124,8 @@ export default function Layout() {
key={item.path}
to={item.path}
className={`group flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${isActive
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 font-semibold shadow-sm'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50'
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 font-semibold shadow-sm'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50'
}`}
aria-current={isActive ? 'page' : undefined}
>
@ -138,29 +143,41 @@ export default function Layout() {
})}
</nav>
{/* User Menu (Desktop) */}
<div className="p-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-3 mb-4 px-2">
<div className="relative w-10 h-10 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-700 flex items-center justify-center text-sm font-bold text-white ring-2 ring-emerald-500/20">
{user?.email[0].toUpperCase()}
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-slate-800" />
</div>
<div className="flex-1 overflow-hidden">
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
{user?.name || user?.email}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400 uppercase">
{user?.role}
</p>
</div>
<div className="relative">
<button
onClick={() => setUserMenuOpen(!userMenuOpen)}
className="w-full flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700/50 transition-colors"
>
<div className="relative w-10 h-10 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-700 flex items-center justify-center text-sm font-bold text-white ring-2 ring-emerald-500/20">
{user?.email[0].toUpperCase()}
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-slate-800" />
</div>
<div className="flex-1 text-left overflow-hidden">
<p className="text-sm font-medium text-slate-900 dark:text-white truncate">
{user?.name || user?.email}
</p>
<p className="text-xs text-slate-600 dark:text-slate-400 uppercase">
{user?.role}
</p>
</div>
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${userMenuOpen ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown Menu */}
{userMenuOpen && (
<div className="absolute bottom-full left-0 right-0 mb-2 bg-white dark:bg-slate-700 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 py-1 animate-scale-in">
<button
onClick={logout}
className="w-full flex items-center gap-2 px-4 py-2 text-sm text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
)}
</div>
<button
onClick={logout}
className="group w-full flex items-center justify-center gap-2 py-2 px-4 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 text-red-700 dark:text-red-400 text-sm font-medium rounded-lg transition-all"
aria-label="Sign out"
>
<LogOut className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
Sign Out
</button>
</div>
</aside>
@ -188,8 +205,8 @@ export default function Layout() {
key={item.path}
to={item.path}
className={`flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all min-w-[64px] ${isActive
? 'text-emerald-600 dark:text-emerald-400'
: 'text-slate-600 dark:text-slate-400'
? 'text-emerald-600 dark:text-emerald-400'
: 'text-slate-600 dark:text-slate-400'
}`}
aria-current={isActive ? 'page' : undefined}
>

View file

@ -0,0 +1,82 @@
import api from './api';
export type SupplyCategory =
| 'FILTER'
| 'CLEANING'
| 'PPE'
| 'OFFICE'
| 'BATHROOM'
| 'KITCHEN'
| 'MAINTENANCE'
| 'OTHER';
export interface SupplyItem {
id: string;
name: string;
category: SupplyCategory;
quantity: number;
minThreshold: number;
unit: string;
location?: string;
vendor?: string;
productUrl?: string;
lastOrdered?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface CreateSupplyItemData {
name: string;
category: SupplyCategory;
quantity: number;
minThreshold: number;
unit: string;
location?: string;
vendor?: string;
productUrl?: string;
notes?: string;
}
export type UpdateSupplyItemData = Partial<CreateSupplyItemData>;
export const suppliesApi = {
getAll: async () => {
const response = await api.get<SupplyItem[]>('/supplies');
return response.data;
},
getShoppingList: async () => {
const response = await api.get<SupplyItem[]>('/supplies/shopping-list');
return response.data;
},
getOne: async (id: string) => {
const response = await api.get<SupplyItem>(`/supplies/${id}`);
return response.data;
},
create: async (data: CreateSupplyItemData) => {
const response = await api.post<SupplyItem>('/supplies', data);
return response.data;
},
update: async (id: string, data: UpdateSupplyItemData) => {
const response = await api.patch<SupplyItem>(`/supplies/${id}`, data);
return response.data;
},
delete: async (id: string) => {
await api.delete(`/supplies/${id}`);
},
markOrdered: async (id: string) => {
const response = await api.post<SupplyItem>(`/supplies/${id}/order`);
return response.data;
},
adjustQuantity: async (id: string, adjustment: number) => {
const response = await api.post<SupplyItem>(`/supplies/${id}/adjust`, { adjustment });
return response.data;
},
};

View file

@ -0,0 +1,414 @@
import { useState, useEffect } from 'react';
import {
Plus,
Search,
ShoppingCart,
Package,
Filter,
AlertCircle,
Check,
ExternalLink
} from 'lucide-react';
import { suppliesApi, SupplyItem, SupplyCategory } from '../lib/suppliesApi';
export default function SuppliesPage() {
const [items, setItems] = useState<SupplyItem[]>([]);
const [loading, setLoading] = useState(true);
const [view, setView] = useState<'all' | 'shopping'>('all');
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState<SupplyCategory | 'ALL'>('ALL');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
// Form State
const [newItem, setNewItem] = useState({
name: '',
category: 'OTHER' as SupplyCategory,
quantity: 0,
minThreshold: 1,
unit: 'each',
location: '',
vendor: '',
productUrl: '',
notes: ''
});
useEffect(() => {
loadItems();
}, []);
const loadItems = async () => {
setLoading(true);
try {
const data = await suppliesApi.getAll();
setItems(data);
} catch (error) {
console.error('Failed to load supplies', error);
} finally {
setLoading(false);
}
};
const handleQuantityAdjust = async (id: string, adjustment: number) => {
// Optimistic update
setItems(current => current.map(item => {
if (item.id === id) {
return { ...item, quantity: Math.max(0, item.quantity + adjustment) };
}
return item;
}));
try {
await suppliesApi.adjustQuantity(id, adjustment);
} catch (error) {
// Revert on error
loadItems();
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
try {
await suppliesApi.create(newItem);
setIsAddModalOpen(false);
setNewItem({
name: '',
category: 'OTHER',
quantity: 0,
minThreshold: 1,
unit: 'each',
location: '',
vendor: '',
productUrl: '',
notes: ''
});
loadItems();
} catch (error) {
console.error(error);
}
};
const handleMarkOrdered = async (id: string) => {
try {
const updated = await suppliesApi.markOrdered(id);
setItems(current => current.map(item => item.id === id ? updated : item));
} catch (error) {
console.error(error);
}
};
// Derived state
const shoppingListCount = items.filter(i => i.quantity <= i.minThreshold).length;
const filteredItems = items.filter(item => {
const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase()) ||
(item.location && item.location.toLowerCase().includes(search.toLowerCase())) ||
(item.vendor && item.vendor.toLowerCase().includes(search.toLowerCase()));
const matchesCategory = categoryFilter === 'ALL' || item.category === categoryFilter;
const matchesView = view === 'all' || (view === 'shopping' && item.quantity <= item.minThreshold);
return matchesSearch && matchesCategory && matchesView;
});
const categories: SupplyCategory[] = ['FILTER', 'CLEANING', 'PPE', 'OFFICE', 'BATHROOM', 'KITCHEN', 'MAINTENANCE', 'OTHER'];
return (
<div className="max-w-6xl mx-auto space-y-6 pb-20">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
{view === 'shopping' ? '🛒 Shopping List' : '📦 Storage & Inventory'}
</h1>
<p className="text-slate-500 dark:text-slate-400 text-sm">
{view === 'shopping'
? `${filteredItems.length} items need attention`
: 'Manage facility supplies and track usage'}
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setView('all')}
className={`flex-1 md:flex-none px-4 py-2 rounded-lg text-sm font-medium transition-colors ${view === 'all'
? 'bg-emerald-600 text-white'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700'
}`}
>
Inventory
</button>
<button
onClick={() => setView('shopping')}
className={`flex-1 md:flex-none px-4 py-2 rounded-lg text-sm font-medium transition-colors relative ${view === 'shopping'
? 'bg-emerald-600 text-white'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700'
}`}
>
Shopping List
{shoppingListCount > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs flex items-center justify-center rounded-full">
{shoppingListCount}
</span>
)}
</button>
<button
onClick={() => setIsAddModalOpen(true)}
className="hidden md:flex items-center gap-2 px-4 py-2 bg-slate-900 dark:bg-slate-700 text-white rounded-lg text-sm font-medium hover:bg-slate-800"
>
<Plus size={16} />
Add Item
</button>
</div>
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4 bg-white dark:bg-slate-800 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="Search supplies, vendors..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 rounded-lg bg-slate-50 dark:bg-slate-900 border-none focus:ring-2 focus:ring-emerald-500 text-slate-900 dark:text-white"
/>
</div>
<div className="flex gap-2 overflow-x-auto pb-2 md:pb-0 no-scrollbar">
<button
onClick={() => setCategoryFilter('ALL')}
className={`px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap ${categoryFilter === 'ALL'
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400'
: 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400'
}`}
>
All
</button>
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategoryFilter(cat)}
className={`px-3 py-2 rounded-lg text-xs font-medium whitespace-nowrap ${categoryFilter === cat
? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-400'
: 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-400'
}`}
>
{cat}
</button>
))}
</div>
</div>
{/* Item List */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredItems.map(item => (
<div
key={item.id}
className={`bg-white dark:bg-slate-800 p-5 rounded-xl border ${item.quantity <= item.minThreshold
? 'border-red-300 dark:border-red-900/50 ring-1 ring-red-100 dark:ring-red-900/20'
: 'border-slate-200 dark:border-slate-700'
} shadow-sm transition-all hover:shadow-md`}
>
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-bold text-slate-900 dark:text-white flex items-center gap-2">
{item.name}
{item.quantity <= item.minThreshold && (
<AlertCircle size={16} className="text-red-500" />
)}
</h3>
<div className="text-xs text-slate-500 dark:text-slate-400 mt-1 flex flex-wrap gap-2">
<span className="bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded capitalize">
{item.category.toLowerCase()}
</span>
{item.location && (
<span className="bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded flex items-center gap-1">
📍 {item.location}
</span>
)}
{item.vendor && (
<span className="bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded flex items-center gap-1">
🏪 {item.vendor}
</span>
)}
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold tabular-nums text-slate-900 dark:text-white">
{item.quantity}
</div>
<div className="text-xs text-slate-500">
{item.unit} (Min: {item.minThreshold})
</div>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-3 mt-4 pt-4 border-t border-slate-100 dark:border-slate-700">
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-900 rounded-lg p-1">
<button
onClick={() => handleQuantityAdjust(item.id, -1)}
className="w-8 h-8 flex items-center justify-center rounded bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 shadow-sm disabled:opacity-50"
disabled={item.quantity <= 0}
>
-
</button>
<button
onClick={() => handleQuantityAdjust(item.id, 1)}
className="w-8 h-8 flex items-center justify-center rounded bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 shadow-sm"
>
+
</button>
</div>
{item.quantity <= item.minThreshold && (
item.productUrl ? (
<a
href={item.productUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 flex items-center justify-center gap-2 h-10 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 text-sm font-medium hover:bg-emerald-100 dark:hover:bg-emerald-900/40 transition-colors"
>
<ExternalLink size={16} />
Order ({item.vendor})
</a>
) : (
<button
onClick={() => handleMarkOrdered(item.id)}
className="flex-1 flex items-center justify-center gap-2 h-10 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm font-medium hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors"
>
{item.lastOrdered && new Date(item.lastOrdered).toDateString() === new Date().toDateString() ? (
<>
<Check size={16} />
Ordered
</>
) : (
<>
<ShoppingCart size={16} />
Add to list
</>
)}
</button>
)
)}
</div>
</div>
))}
</div>
{/* Mobile FAB */}
<button
onClick={() => setIsAddModalOpen(true)}
className="md:hidden fixed bottom-20 right-4 w-14 h-14 bg-emerald-600 text-white rounded-full shadow-lg flex items-center justify-center z-40 hover:bg-emerald-700 active:scale-95 transition-all"
>
<Plus size={24} />
</button>
{/* Add Item Modal */}
{isAddModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white dark:bg-slate-800 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-scale-in max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-800 z-10">
<h2 className="text-xl font-bold dark:text-white">Add New Item</h2>
<button onClick={() => setIsAddModalOpen(false)} className="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
</button>
</div>
<form onSubmit={handleCreate} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Item Name</label>
<input
required
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.name}
onChange={e => setNewItem({ ...newItem, name: e.target.value })}
placeholder="e.g. 5 Gallon Pots"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Category</label>
<select
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.category}
onChange={e => setNewItem({ ...newItem, category: e.target.value as any })}
>
{categories.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Unit</label>
<input
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.unit}
onChange={e => setNewItem({ ...newItem, unit: e.target.value })}
placeholder="box, each..."
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Current Qty</label>
<input
type="number"
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.quantity}
onChange={e => setNewItem({ ...newItem, quantity: parseInt(e.target.value) || 0 })}
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Min Threshold</label>
<input
type="number"
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.minThreshold}
onChange={e => setNewItem({ ...newItem, minThreshold: parseInt(e.target.value) || 0 })}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Vendor (Optional)</label>
<input
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.vendor}
onChange={e => setNewItem({ ...newItem, vendor: e.target.value })}
placeholder="e.g. Amazon"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Product URL</label>
<input
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.productUrl}
onChange={e => setNewItem({ ...newItem, productUrl: e.target.value })}
placeholder="https://..."
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Location</label>
<input
className="w-full p-3 rounded-lg bg-slate-50 dark:bg-slate-900 border dark:border-slate-700 dark:text-white"
value={newItem.location}
onChange={e => setNewItem({ ...newItem, location: e.target.value })}
placeholder="e.g. Storage Room A"
/>
</div>
<button
type="submit"
className="w-full py-4 bg-emerald-600 hover:bg-emerald-700 text-white font-bold rounded-xl shadow-lg mt-4"
>
Create Item
</button>
</form>
</div>
</div>
)}
</div>
);
}

View file

@ -6,6 +6,7 @@ import DailyWalkthroughPage from './pages/DailyWalkthroughPage';
import RoomsPage from './pages/RoomsPage';
import BatchesPage from './pages/BatchesPage';
import TimeclockPage from './pages/TimeclockPage';
import SuppliesPage from './pages/SuppliesPage';
export const router = createBrowserRouter([
{
@ -36,6 +37,10 @@ export const router = createBrowserRouter([
path: 'timeclock',
element: <TimeclockPage />,
},
{
path: 'supplies',
element: <SuppliesPage />,
},
],
},
]);