Link homepage cards to products views, add url params for views and for login redirect
This commit is contained in:
@@ -11,11 +11,11 @@ import PurchaseOrders from './pages/PurchaseOrders';
|
|||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
|
import { RequireAuth } from './components/auth/RequireAuth';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true';
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,8 +50,11 @@ function App() {
|
|||||||
<Toaster richColors position="top-center" />
|
<Toaster richColors position="top-center" />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
{isLoggedIn ? (
|
<Route element={
|
||||||
<Route element={<MainLayout />}>
|
<RequireAuth>
|
||||||
|
<MainLayout />
|
||||||
|
</RequireAuth>
|
||||||
|
}>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/products" element={<Products />} />
|
<Route path="/products" element={<Products />} />
|
||||||
<Route path="/orders" element={<Orders />} />
|
<Route path="/orders" element={<Orders />} />
|
||||||
@@ -60,9 +63,6 @@ function App() {
|
|||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
) : (
|
|
||||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
|
||||||
)}
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
16
inventory/src/components/auth/RequireAuth.tsx
Normal file
16
inventory/src/components/auth/RequireAuth.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Navigate, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
|
const isLoggedIn = sessionStorage.getItem("isLoggedIn") === "true"
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
// Redirect to login with the current path in the redirect parameter
|
||||||
|
return <Navigate
|
||||||
|
to={`/login?redirect=${encodeURIComponent(location.pathname + location.search)}`}
|
||||||
|
replace
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { useQuery } from "@tanstack/react-query"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch } from "lucide-react"
|
import { AlertCircle, AlertTriangle, CheckCircle2, PackageSearch } from "lucide-react"
|
||||||
import config from "@/config"
|
import config from "@/config"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
interface InventoryHealth {
|
interface InventoryHealth {
|
||||||
critical: number
|
critical: number
|
||||||
@@ -12,6 +14,7 @@ interface InventoryHealth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function InventoryHealthSummary() {
|
export function InventoryHealthSummary() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const { data: summary } = useQuery<InventoryHealth>({
|
const { data: summary } = useQuery<InventoryHealth>({
|
||||||
queryKey: ["inventory-health"],
|
queryKey: ["inventory-health"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -31,6 +34,7 @@ export function InventoryHealthSummary() {
|
|||||||
icon: AlertCircle,
|
icon: AlertCircle,
|
||||||
className: "bg-destructive/10",
|
className: "bg-destructive/10",
|
||||||
iconClassName: "text-destructive",
|
iconClassName: "text-destructive",
|
||||||
|
view: "critical"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Reorder Soon",
|
title: "Reorder Soon",
|
||||||
@@ -39,6 +43,7 @@ export function InventoryHealthSummary() {
|
|||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
className: "bg-warning/10",
|
className: "bg-warning/10",
|
||||||
iconClassName: "text-warning",
|
iconClassName: "text-warning",
|
||||||
|
view: "reorder"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Healthy Stock",
|
title: "Healthy Stock",
|
||||||
@@ -47,6 +52,7 @@ export function InventoryHealthSummary() {
|
|||||||
icon: CheckCircle2,
|
icon: CheckCircle2,
|
||||||
className: "bg-success/10",
|
className: "bg-success/10",
|
||||||
iconClassName: "text-success",
|
iconClassName: "text-success",
|
||||||
|
view: "healthy"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Overstock",
|
title: "Overstock",
|
||||||
@@ -55,13 +61,18 @@ export function InventoryHealthSummary() {
|
|||||||
icon: PackageSearch,
|
icon: PackageSearch,
|
||||||
className: "bg-muted",
|
className: "bg-muted",
|
||||||
iconClassName: "text-muted-foreground",
|
iconClassName: "text-muted-foreground",
|
||||||
|
view: "overstocked"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{stats.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<Card key={stat.title} className={stat.className}>
|
<Card
|
||||||
|
key={stat.title}
|
||||||
|
className={cn(stat.className, "cursor-pointer hover:opacity-90 transition-opacity")}
|
||||||
|
onClick={() => navigate(`/products?view=${stat.view}`)}
|
||||||
|
>
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
|
||||||
<stat.icon className={`h-4 w-4 ${stat.iconClassName}`} />
|
<stat.icon className={`h-4 w-4 ${stat.iconClassName}`} />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -15,6 +15,7 @@ export function Login() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -57,7 +58,12 @@ export function Login() {
|
|||||||
sessionStorage.setItem("token", data.token);
|
sessionStorage.setItem("token", data.token);
|
||||||
sessionStorage.setItem("isLoggedIn", "true");
|
sessionStorage.setItem("isLoggedIn", "true");
|
||||||
toast.success("Successfully logged in");
|
toast.success("Successfully logged in");
|
||||||
navigate("/", { replace: true });
|
|
||||||
|
// Get the redirect URL from the URL parameters, defaulting to "/"
|
||||||
|
const redirectTo = searchParams.get("redirect") || "/"
|
||||||
|
|
||||||
|
// Navigate to the redirect URL after successful login
|
||||||
|
navigate(redirectTo)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { ProductFilters } from '@/components/products/ProductFilters';
|
import { ProductFilters } from '@/components/products/ProductFilters';
|
||||||
import { ProductTable } from '@/components/products/ProductTable';
|
import { ProductTable } from '@/components/products/ProductTable';
|
||||||
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
import { ProductTableSkeleton } from '@/components/products/ProductTableSkeleton';
|
||||||
@@ -151,11 +152,12 @@ const VIEW_COLUMNS: Record<string, ColumnKey[]> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function Products() {
|
export function Products() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [filters, setFilters] = useState<Record<string, string | number | boolean>>({});
|
const [filters, setFilters] = useState<Record<string, string | number | boolean>>({});
|
||||||
const [sortColumn, setSortColumn] = useState<ColumnKey>('title');
|
const [sortColumn, setSortColumn] = useState<ColumnKey>('title');
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [activeView, setActiveView] = useState("all");
|
const [activeView, setActiveView] = useState(searchParams.get('view') || "all");
|
||||||
const [pageSize] = useState(50);
|
const [pageSize] = useState(50);
|
||||||
const [showNonReplenishable, setShowNonReplenishable] = useState(false);
|
const [showNonReplenishable, setShowNonReplenishable] = useState(false);
|
||||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
||||||
@@ -369,6 +371,25 @@ export function Products() {
|
|||||||
: [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2]
|
: [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2]
|
||||||
: Array.from({ length: totalPages }, (_, i) => i + 1);
|
: Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
|
||||||
|
// Update URL when view changes
|
||||||
|
const handleViewChange = (view: string) => {
|
||||||
|
setActiveView(view);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchParams(prev => {
|
||||||
|
const newParams = new URLSearchParams(prev);
|
||||||
|
newParams.set('view', view);
|
||||||
|
return newParams;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync with URL params when they change
|
||||||
|
useEffect(() => {
|
||||||
|
const viewParam = searchParams.get('view');
|
||||||
|
if (viewParam && viewParam !== activeView) {
|
||||||
|
setActiveView(viewParam);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@@ -380,10 +401,10 @@ export function Products() {
|
|||||||
<h1 className="text-3xl font-bold tracking-tight">Products</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Products</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProductViews activeView={activeView} onViewChange={(view) => {
|
<ProductViews
|
||||||
setActiveView(view);
|
activeView={activeView}
|
||||||
setCurrentPage(1);
|
onViewChange={handleViewChange}
|
||||||
}} />
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user