diff --git a/app/dashboard/admin/ban/page.tsx b/app/dashboard/admin/ban/page.tsx index 6add508..1941368 100644 --- a/app/dashboard/admin/ban/page.tsx +++ b/app/dashboard/admin/ban/page.tsx @@ -32,6 +32,7 @@ export default function AdminBanPage() { const [unbanning, setUnbanning] = useState(null); const [blockedUsers, setBlockedUsers] = useState([]); const [searchQuery, setSearchQuery] = useState(""); + const [banDialogOpen, setBanDialogOpen] = useState(false); const [banData, setBanData] = useState({ telegramUserId: "", reason: "", @@ -59,7 +60,10 @@ export default function AdminBanPage() { } }; - const handleBanUser = async () => { + const handleBanUser = async (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + if (!banData.telegramUserId) { toast({ title: "Error", @@ -71,15 +75,12 @@ export default function AdminBanPage() { try { setBanning(true); - await fetchClient("/admin/ban", { + const response = await fetchClient("/admin/ban", { method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ + body: { telegramUserId: parseInt(banData.telegramUserId), reason: banData.additionalDetails || banData.reason || undefined, - }), + }, }); toast({ @@ -88,7 +89,8 @@ export default function AdminBanPage() { }); setBanData({ telegramUserId: "", reason: "", additionalDetails: "" }); - fetchBlockedUsers(); + setBanDialogOpen(false); + await fetchBlockedUsers(); } catch (error: any) { console.error("Failed to ban user:", error); toast({ @@ -101,7 +103,10 @@ export default function AdminBanPage() { } }; - const handleUnbanUser = async (telegramUserId: number) => { + const handleUnbanUser = async (telegramUserId: number, e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + try { setUnbanning(telegramUserId.toString()); await fetchClient(`/admin/ban/${telegramUserId}`, { @@ -113,7 +118,7 @@ export default function AdminBanPage() { description: "User has been unbanned", }); - fetchBlockedUsers(); + await fetchBlockedUsers(); } catch (error: any) { console.error("Failed to unban user:", error); toast({ @@ -235,20 +240,18 @@ export default function AdminBanPage() {
- + { + if (!banning) { + setBanDialogOpen(open); + } + }} + > @@ -259,9 +262,23 @@ export default function AdminBanPage() { - Cancel - - Confirm Ban + Cancel + { + e.preventDefault(); + handleBanUser(e); + }} + disabled={banning} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {banning ? ( + <> + + Banning... + + ) : ( + "Confirm Ban" + )} @@ -354,9 +371,19 @@ export default function AdminBanPage() { - Cancel - handleUnbanUser(user.telegramUserId)}> - Confirm Unban + Cancel + handleUnbanUser(user.telegramUserId, e)} + disabled={unbanning === user.telegramUserId.toString()} + > + {unbanning === user.telegramUserId.toString() ? ( + <> + + Unbanning... + + ) : ( + "Confirm Unban" + )} diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index bf2237a..1b7c9b5 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -282,6 +282,31 @@ export default function ProductsPage() { setAddProductOpen(true); }; + // Clone product - opens modal with product data but as a new product + const handleCloneProduct = (product: Product) => { + // Clone the product but remove _id and image to make it a new product + const clonedProduct: Product = { + ...product, + _id: undefined, // Remove ID so it's treated as a new product + name: `${product.name} (Copy)`, // Add "(Copy)" to the name + image: null, // Clear image so user can upload a new one + pricing: product.pricing + ? product.pricing.map((tier) => ({ + minQuantity: tier.minQuantity, + pricePerUnit: tier.pricePerUnit, + })) + : [{ minQuantity: 1, pricePerUnit: 0 }], + costPerUnit: product.costPerUnit || 0, + // Reset stock to defaults for cloned product + currentStock: 0, + stockStatus: 'out_of_stock' as const, + }; + + setProductData(clonedProduct); + setEditing(false); // Set to false so it creates a new product + setAddProductOpen(true); + }; + // Reset product data when adding a new product const handleAddNewProduct = () => { setProductData({ @@ -435,6 +460,7 @@ export default function ProductsPage() { products={filteredProducts} loading={loading} onEdit={handleEditProduct} + onClone={handleCloneProduct} onDelete={handleDeleteProduct} onToggleEnabled={handleToggleEnabled} onProfitAnalysis={handleProfitAnalysis} diff --git a/components/admin/BanUserCard.tsx b/components/admin/BanUserCard.tsx index 9ddcf0e..7ff4cf4 100644 --- a/components/admin/BanUserCard.tsx +++ b/components/admin/BanUserCard.tsx @@ -13,9 +13,20 @@ export default function BanUserCard() { setLoading(true); setMessage(null); try { + const userId = parseInt(telegramUserId); + if (isNaN(userId)) { + setMessage("Invalid Telegram User ID"); + return; + } await fetchClient("/admin/ban", { method: "POST", - body: { telegramUserId, reason } + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + telegramUserId: userId, + reason: reason || undefined + }) }); setMessage("User banned"); setTelegramUserId(""); diff --git a/components/dashboard/ChatDetail.tsx b/components/dashboard/ChatDetail.tsx index 017d176..509548f 100644 --- a/components/dashboard/ChatDetail.tsx +++ b/components/dashboard/ChatDetail.tsx @@ -277,9 +277,17 @@ export default function ChatDetail({ chatId }: { chatId: string }) { setTimeout(() => { scrollToBottomHandler(); }, 100); - } catch (error) { + } catch (error: any) { console.error("Error fetching chat data:", error); - toast.error("Failed to load chat"); + + // Don't redirect on auth errors - let the middleware handle it + // Only show error toast for non-auth errors + if (error?.message?.includes('401') || error?.message?.includes('403')) { + // Auth errors will be handled by middleware, don't show toast + console.log("Auth error detected, middleware will handle redirect"); + } else { + toast.error("Failed to load chat"); + } } finally { setLoading(false); } @@ -352,8 +360,14 @@ export default function ChatDetail({ chatId }: { chatId: string }) { }, 1000); } } - } catch (error) { + } catch (error: any) { console.error("Error polling new messages:", error); + + // Silently fail on auth errors during polling - don't disrupt the user + if (error?.message?.includes('401') || error?.message?.includes('403')) { + console.log("Auth error during polling, stopping poll"); + return; + } } finally { isPollingRef.current = false; } diff --git a/components/tables/product-table.tsx b/components/tables/product-table.tsx index 7caf90e..04096fd 100644 --- a/components/tables/product-table.tsx +++ b/components/tables/product-table.tsx @@ -1,5 +1,5 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator } from "lucide-react"; +import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator, Copy } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Product } from "@/models/products"; import { Badge } from "@/components/ui/badge"; @@ -9,6 +9,7 @@ interface ProductTableProps { products: Product[]; loading: boolean; onEdit: (product: Product) => void; + onClone?: (product: Product) => void; onDelete: (productId: string) => void; onToggleEnabled: (productId: string, enabled: boolean) => void; onProfitAnalysis?: (productId: string, productName: string) => void; @@ -19,6 +20,7 @@ const ProductTable = ({ products, loading, onEdit, + onClone, onDelete, onToggleEnabled, onProfitAnalysis, @@ -109,7 +111,18 @@ const ProductTable = ({ )} - + )} + diff --git a/middleware.ts b/middleware.ts index 52ffc51..2d628c3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -46,12 +46,28 @@ export async function middleware(req: NextRequest) { headers.set('Authorization', `Bearer ${token}`); } - const res = await fetch(authCheckUrl, { - method: "GET", - headers, - credentials: 'include', - signal: AbortSignal.timeout(10000), // 10 second timeout - }); + let res: Response; + try { + res = await fetch(authCheckUrl, { + method: "GET", + headers, + credentials: 'include', + signal: AbortSignal.timeout(15000), // 15 second timeout (increased for slower connections) + }); + } catch (fetchError) { + // Handle timeout or network errors gracefully + console.error("Middleware: Auth check request failed:", fetchError); + + // If it's a timeout or network error, don't redirect - let the request proceed + // The page will handle auth errors client-side + if (fetchError instanceof Error && fetchError.name === 'TimeoutError') { + console.log("Middleware: Auth check timed out, allowing request to proceed"); + return NextResponse.next(); + } + + // For other network errors, redirect to login + return NextResponse.redirect(new URL("/auth/login", req.url)); + } console.log(`Middleware: Auth check responded with status ${res.status}`); @@ -63,9 +79,11 @@ export async function middleware(req: NextRequest) { console.log("Middleware: Auth check successful"); // Admin-only protection for /dashboard/admin routes + // Clone the response before reading it to avoid consuming the body if (pathname.startsWith('/dashboard/admin')) { try { - const user = await res.json(); + const clonedRes = res.clone(); + const user = await clonedRes.json(); const username = user?.vendor?.username; if (username !== 'admin1') { console.log("Middleware: Non-admin attempted to access /dashboard/admin, redirecting"); diff --git a/public/git-info.json b/public/git-info.json index e01af18..b85f8f5 100644 --- a/public/git-info.json +++ b/public/git-info.json @@ -1,4 +1,4 @@ { - "commitHash": "93ec3d3", - "buildTime": "2025-12-17T23:21:50.682Z" + "commitHash": "2db13cc", + "buildTime": "2025-12-27T20:56:32.712Z" } \ No newline at end of file