diff --git a/app/dashboard/categories/page.tsx b/app/dashboard/categories/page.tsx new file mode 100644 index 0000000..aba6ab1 --- /dev/null +++ b/app/dashboard/categories/page.tsx @@ -0,0 +1,282 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Layout from "@/components/layout/layout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Plus, Pencil, Trash2, ChevronRight, ChevronDown } from "lucide-react"; +import { toast } from "sonner"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { apiRequest } from "@/lib/storeHelper"; +import type { Category } from "@/models/categories"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function CategoriesPage() { + const [categories, setCategories] = useState([]); + const [newCategoryName, setNewCategoryName] = useState(""); + const [selectedParentId, setSelectedParentId] = useState(""); + const [editingCategory, setEditingCategory] = useState(null); + const [categoryToDelete, setCategoryToDelete] = useState(null); + + const rootCategories = categories.filter(cat => !cat.parentId); + + const getSubcategories = (parentId: string) => + categories.filter(cat => cat.parentId === parentId); + + useEffect(() => { + fetchCategories(); + }, []); + + const fetchCategories = async () => { + try { + const fetchedCategories = await apiRequest("/categories", "GET"); + setCategories(fetchedCategories); + } catch (error) { + toast.error("Failed to fetch categories"); + } + }; + + const handleAddCategory = async () => { + if (!newCategoryName.trim()) { + toast.error("Category name cannot be empty"); + return; + } + + try { + const response = await apiRequest("/categories", "POST", { + name: newCategoryName, + parentId: selectedParentId || undefined, + }); + + setCategories([...categories, response]); + setNewCategoryName(""); + setSelectedParentId(""); + toast.success("Category added successfully"); + } catch (error) { + toast.error("Failed to add category"); + } + }; + + const handleUpdateCategory = async (categoryId: string, newName: string) => { + try { + const response = await apiRequest(`/categories/${categoryId}`, "PUT", { + name: newName, + }); + + setCategories(categories.map(cat => + cat._id === categoryId ? { ...cat, name: newName } : cat + )); + setEditingCategory(null); + toast.success("Category updated successfully"); + } catch (error) { + toast.error("Failed to update category"); + } + }; + + const handleDeleteConfirm = async () => { + if (!categoryToDelete) return; + + try { + await apiRequest(`/categories/${categoryToDelete._id}`, "DELETE"); + setCategories(categories.filter(cat => cat._id !== categoryToDelete._id)); + toast.success("Category deleted successfully"); + setCategoryToDelete(null); + } catch (error) { + console.error("Failed to delete category:", error); + toast.error("Failed to delete category"); + } + }; + + const renderCategoryItem = (category: Category, level: number = 0) => { + const subcategories = getSubcategories(category._id); + const hasSubcategories = subcategories.length > 0; + const isEditing = editingCategory?._id === category._id; + + return ( +
+
+ {hasSubcategories && ( + + )} +
+ {isEditing ? ( + setEditingCategory(prev => prev ? { ...prev, name: e.target.value } : prev)} + className="h-8 max-w-[200px]" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter' && editingCategory) { + handleUpdateCategory(category._id, editingCategory.name); + } else if (e.key === 'Escape') { + setEditingCategory(null); + } + }} + /> + ) : ( + {category.name} + )} +
+
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ {subcategories.map(subcat => renderCategoryItem(subcat, level + 1))} +
+ ); + }; + + return ( + +
+
+

Categories

+
+ + + + Add New Category + + +
+
+ + setNewCategoryName(e.target.value)} + placeholder="Enter category name" + className="h-9" + /> +
+
+ + +
+ +
+
+
+ + + + Category List + + +
+ {rootCategories.length === 0 ? ( +

+ No categories yet. Add your first category above. +

+ ) : ( + rootCategories.map(category => renderCategoryItem(category)) + )} +
+
+
+ + {/* Delete Confirmation Dialog */} + setCategoryToDelete(null)}> + + + Are you absolutely sure? + + This will permanently delete the category "{categoryToDelete?.name}" + {getSubcategories(categoryToDelete?._id || "").length > 0 && + " and all its subcategories"}. This action cannot be undone. + + + + Cancel + + Delete + + + + +
+
+ ); +} \ No newline at end of file diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index ac4e6fa..4473a93 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -14,11 +14,13 @@ import { } from "@/lib/productData"; import { ProductModal } from "@/components/modals/product-modal"; import ProductTable from "@/components/tables/product-table"; +import { Category } from "@/models/categories" + export default function ProductsPage() { const router = useRouter(); const [products, setProducts] = useState([]); - const [categories, setCategories] = useState([]); + const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(false); @@ -47,14 +49,8 @@ export default function ProductsPage() { const fetchDataAsync = async () => { try { const [fetchedProducts, fetchedCategories] = await Promise.all([ - fetchProductData( - `${process.env.NEXT_PUBLIC_API_URL}/products`, - authToken - ), - fetchProductData( - `${process.env.NEXT_PUBLIC_API_URL}/categories`, - authToken - ), + fetchProductData(`${process.env.NEXT_PUBLIC_API_URL}/products`, authToken), + fetchProductData(`${process.env.NEXT_PUBLIC_API_URL}/categories`, authToken), ]); console.log("Fetched Products:", fetchedProducts); @@ -207,7 +203,12 @@ export default function ProductsPage() { // Get category name by ID const getCategoryNameById = (categoryId: string): string => { const category = categories.find((cat) => cat._id === categoryId); - return category ? category.name : "Unknown Category"; + if (!category) return "Unknown Category"; + if (category.parentId) { + const parent = categories.find((cat) => cat._id === category.parentId); + return parent ? `${parent.name} > ${category.name}` : category.name; + } + return category.name; }; return ( diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx index 67e6e00..ccef8e8 100644 --- a/components/layout/layout.tsx +++ b/components/layout/layout.tsx @@ -21,7 +21,7 @@ export default function Layout({ children }: LayoutProps) {
-
{children}
+
{children}
) diff --git a/components/layout/nav-item.tsx b/components/layout/nav-item.tsx index 0286537..9e4d270 100644 --- a/components/layout/nav-item.tsx +++ b/components/layout/nav-item.tsx @@ -1,6 +1,6 @@ import Link from "next/link" import type { LucideIcon } from "lucide-react" -import type React from "react" // Added import for React +import type React from "react" interface NavItemProps { href: string diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index e99a853..7fceb55 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -8,7 +8,9 @@ import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, + SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; @@ -21,10 +23,10 @@ import { Plus } from "lucide-react"; import { apiRequest } from "@/lib/storeHelper"; type CategorySelectProps = { - categories: { _id: string; name: string }[]; + categories: { _id: string; name: string; parentId?: string }[]; value: string; setProductData: React.Dispatch>; - onAddCategory: (newCategory: { _id: string; name: string }) => void; + onAddCategory: (newCategory: { _id: string; name: string; parentId?: string }) => void; }; export const ProductModal: React.FC = ({ @@ -101,7 +103,7 @@ export const ProductModal: React.FC = ({ onClose(); }; - const handleAddCategory = (newCategory: { _id: string; name: string }) => { + const handleAddCategory = (newCategory: { _id: string; name: string; parentId?: string }) => { setLocalCategories((prev) => [...prev, newCategory]); }; @@ -167,9 +169,9 @@ export const ProductModal: React.FC = ({ const ProductBasicInfo: React.FC<{ productData: ProductData; handleChange: (e: React.ChangeEvent) => void; - categories: { _id: string; name: string }[]; + categories: { _id: string; name: string; parentId?: string }[]; setProductData: React.Dispatch>; - onAddCategory: (newCategory: { _id: string; name: string }) => void; + onAddCategory: (newCategory: { _id: string; name: string; parentId?: string }) => void; }> = ({ productData, handleChange, categories, setProductData, onAddCategory }) => (
@@ -205,6 +207,7 @@ const ProductBasicInfo: React.FC<{
); @@ -213,80 +216,65 @@ const CategorySelect: React.FC = ({ categories, value, setProductData, - onAddCategory, }) => { - const [isAddingCategory, setIsAddingCategory] = useState(false); - const [newCategoryName, setNewCategoryName] = useState(""); + const [selectedMainCategory, setSelectedMainCategory] = useState(""); - const handleAddCategory = async () => { - if (!newCategoryName.trim()) { - toast.error("Category name cannot be empty"); - return; - } - - try { - const response = await apiRequest("/categories", "POST", { name: newCategoryName }); - const newCategory = response.data; - setProductData((prev) => ({ ...prev, category: newCategory._id })); - onAddCategory(newCategory); - setIsAddingCategory(false); - setNewCategoryName(""); - toast.success("New category added successfully"); - } catch (error) { - console.error("Failed to add new category:", error); - toast.error("Failed to add new category"); - } - }; + // Get root categories (those without parentId) + const rootCategories = categories.filter(cat => !cat.parentId); + + // Get subcategories for a given parent + const getSubcategories = (parentId: string) => + categories.filter(cat => cat.parentId === parentId); return (
-
- {isAddingCategory ? ( - setNewCategoryName(e.target.value)} - placeholder="New category name" - className="flex-grow h-9 text-sm" - /> - ) : ( +
+ {/* Main Category Select */} + + + {/* Subcategory Select */} + {selectedMainCategory && selectedMainCategory !== "uncategorized" && ( )} -
); @@ -295,7 +283,8 @@ const CategorySelect: React.FC = ({ const UnitTypeSelect: React.FC<{ value: string; setProductData: React.Dispatch>; -}> = ({ value, setProductData }) => ( + categories: { _id: string; name: string; parentId?: string }[]; +}> = ({ value, setProductData, categories }) => (
diff --git a/config/sidebar.ts b/config/sidebar.ts index ce18c6e..c55f5df 100644 --- a/config/sidebar.ts +++ b/config/sidebar.ts @@ -1,4 +1,4 @@ -import { Home, Package, Box, Truck, Settings } from "lucide-react" +import { Home, Package, Box, Truck, Settings, FolderTree } from "lucide-react" export const sidebarConfig = [ { @@ -12,6 +12,7 @@ export const sidebarConfig = [ title: "Management", items: [ { name: "Products", href: "/dashboard/products", icon: Box }, + { name: "Categories", href: "/dashboard/categories", icon: FolderTree}, { name: "Shipping", href: "/dashboard/shipping", icon: Truck }, { name: "Storefront", href: "/dashboard/storefront", icon: Settings }, ], diff --git a/lib/server-service.ts b/lib/server-service.ts index 3593e5d..9466a1f 100644 --- a/lib/server-service.ts +++ b/lib/server-service.ts @@ -14,6 +14,7 @@ export async function fetchServer( if (!authToken) redirect('/login'); try { + console.log(`${endpoint}`) const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, { ...options, headers: { @@ -29,7 +30,7 @@ export async function fetchServer( return res.json(); } catch (error) { - console.error(`Server request to ${endpoint} failed:`, error); + //console.error(`Server request to ${endpoint} failed:`, error); throw error; } } \ No newline at end of file diff --git a/lib/storeHelper.ts b/lib/storeHelper.ts index d927403..ddb4167 100644 --- a/lib/storeHelper.ts +++ b/lib/storeHelper.ts @@ -16,7 +16,6 @@ export const apiRequest = async (endpoint: string, method: string = "GE throw new Error("No authentication token found"); } - // ✅ API Request Options const options: RequestInit = { method, headers: { @@ -41,7 +40,6 @@ export const apiRequest = async (endpoint: string, method: string = "GE throw new Error(`Failed to ${method} ${endpoint}: ${errorMessage}`); } - // ✅ Return JSON response return res; } catch (error: unknown) { if (error instanceof Error) { diff --git a/lib/styles.ts b/lib/styles.ts index afd9d32..fc60def 100644 --- a/lib/styles.ts +++ b/lib/styles.ts @@ -1,7 +1,6 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; - /** * Utility function for merging Tailwind CSS class names with conditional logic. * @param inputs - Class values to merge. diff --git a/lib/types.ts b/lib/types.ts index 702152b..4bb76b0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -3,11 +3,11 @@ import type React from "react" export interface ProductModalProps { open: boolean - onClose: () => void - onSave: (productData: ProductData, imageFile?: File | null) => void; productData: ProductData categories: Category[] editing: boolean + onClose: () => void + onSave: (productData: ProductData, imageFile?: File | null) => void; handleChange: (e: React.ChangeEvent) => void handleTieredPricingChange: (e: React.ChangeEvent, index: number) => void handleAddTier: () => void diff --git a/models/categories.ts b/models/categories.ts new file mode 100644 index 0000000..4920d41 --- /dev/null +++ b/models/categories.ts @@ -0,0 +1,5 @@ +export interface Category { + _id: string; + name: string; + parentId?: string + } \ No newline at end of file