= ({
+ brands,
+ value,
+ onBrandChange,
+ onBrandCreate,
+}) => {
+ const [newBrandName, setNewBrandName] = useState("");
+ const [creating, setCreating] = useState(false);
+
+ const handleCreateBrand = async () => {
+ if (!newBrandName.trim() || !onBrandCreate) return;
+
+ setCreating(true);
+ try {
+ const createdBrand = await onBrandCreate(newBrandName);
+ onBrandChange(createdBrand);
+ setNewBrandName("");
+ } catch (error) {
+ console.error("Error creating brand:", error);
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/AllProducts.tsx b/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/AllProducts.tsx
index 458424e..3ea9bce 100644
--- a/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/AllProducts.tsx
+++ b/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/AllProducts.tsx
@@ -10,6 +10,8 @@ import {
Text,
Stack,
Table,
+ Badge,
+ SegmentedControl,
} from "@mantine/core";
import { apiService } from "../../../../config/api";
@@ -46,9 +48,18 @@ const AllProducts = () => {
const [Products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [view, setView] = useState<"table" | "card">("card");
+ const [visibilityFilter, setVisibilityFilter] = useState<
+ "all" | "visible" | "hidden"
+ >("all");
const hasFetched = useRef(false);
const navigate = useNavigate();
+ const filteredProducts = Products.filter((product) => {
+ if (visibilityFilter === "visible") return product.isVisible;
+ if (visibilityFilter === "hidden") return !product.isVisible;
+ return true;
+ });
+
const fetchData = async () => {
try {
const data: Product[] = await apiService.get("/Product");
@@ -73,6 +84,7 @@ const AllProducts = () => {
hasFetched.current = true;
}
}, []);
+
const getProductImageUrl = (product: Product) => {
const primaryPhoto =
product.photos?.find((p) => p.isPrimary) || product.photos?.[0];
@@ -106,13 +118,33 @@ const AllProducts = () => {
+
+
+ setVisibilityFilter(value as "all" | "visible" | "hidden")
+ }
+ data={[
+ { label: `All (${Products.length})`, value: "all" },
+ {
+ label: `Visible (${Products.filter((p) => p.isVisible).length})`,
+ value: "visible",
+ },
+ {
+ label: `Hidden (${Products.filter((p) => !p.isVisible).length})`,
+ value: "hidden",
+ },
+ ]}
+ />
+
+
{view === "card" ? (
- {Products.map((p) => (
+ {filteredProducts.map((p) => (
{
- {p.title}
+
+ {p.title}
+ {!p.isVisible && (
+
+ Hidden
+
+ )}
+
${p.price.toFixed(2)}
@@ -164,19 +203,35 @@ const AllProducts = () => {
>
- Name
- Price
- Stock
+ Name
+ Visibility
+
+ Price
+
+
+ Stock
+
- {Products.map((p) => (
+ {filteredProducts.map((p) => (
navigate(`/admin/products/${p.id}`)}
style={{ cursor: "pointer" }}
>
{p.title}
+
+ {p.isVisible ? (
+
+ Visible
+
+ ) : (
+
+ Hidden
+
+ )}
+
${p.price.toFixed(2)}
0 ? "green" : "red"}>
@@ -190,6 +245,12 @@ const AllProducts = () => {
)}
+
+ {filteredProducts.length === 0 && (
+
+ No products found for the selected filter.
+
+ )}
);
};
diff --git a/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/ProductDetailAdminPage.tsx b/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/ProductDetailAdminPage.tsx
index 1333873..a1c9712 100644
--- a/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/ProductDetailAdminPage.tsx
+++ b/frontend/ATSYN-client/src/pages/admin/adminpages/ProductManagement/ProductDetailAdminPage.tsx
@@ -4,6 +4,7 @@ import {
Image,
Title,
TextInput,
+ Textarea,
NumberInput,
Stack,
Button,
@@ -15,18 +16,30 @@ import {
Loader,
Center,
Paper,
+ Select,
+ Divider,
+ ActionIcon,
+ Checkbox,
+ FileInput,
+ Text,
} from "@mantine/core";
import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import "./ProductDetailAdminPage.css";
import { apiService } from "../../../../config/api";
import { CategorySelect } from "../CategoryManagement/CategorySelect";
-//port { CategorySelect } from "CategorySelect";
-
+import { IconUpload, IconTrash } from "@tabler/icons-react";
+import { BrandSelect } from "../CategoryManagement/BrandSelect";
interface Category {
id: number;
name: string;
+ parentCategoryId?: number | null;
+}
+
+interface Brand {
+ id: number;
+ name: string;
}
interface Photo {
@@ -41,19 +54,48 @@ interface Photo {
imageUrl: string;
}
+interface AttributeOption {
+ id: number;
+ value: string;
+ displayOrder: number;
+}
+
+interface CategoryAttribute {
+ id: number;
+ name: string;
+ type: string;
+ categoryId: number;
+ isRequired: boolean;
+ displayOrder: number;
+ isVisibleToCustomers: boolean;
+ options: AttributeOption[];
+}
+
+interface ProductAttributeValue {
+ id?: number;
+ attributeId: number;
+ attributeName?: string;
+ value: string;
+ price?: number;
+ stockAmount?: number;
+}
+
interface Product {
id: number;
title: string;
description: string;
price: number;
categoryId: number;
+ brandId?: number | null;
stockAmount: number;
isVisible: boolean;
shippingTypeId: number;
inStock: boolean;
imageUrl: string;
category: Category;
+ brand?: Brand;
photos: Photo[];
+ attributeValues: ProductAttributeValue[];
}
const shippingInfo =
@@ -67,22 +109,86 @@ const ProductDetailAdminPage = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState(null);
+ const [categories, setCategories] = useState([]);
+ const [brands, setBrands] = useState([]);
+ const [categoryAttributes, setCategoryAttributes] = useState<
+ CategoryAttribute[]
+ >([]);
+ const [selectedAttributes, setSelectedAttributes] = useState<
+ ProductAttributeValue[]
+ >([]);
+ const [selectedRootCategoryId, setSelectedRootCategoryId] = useState<
+ number | null
+ >(null);
+ const [photos, setPhotos] = useState([]);
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
useEffect(() => {
- const fetchProduct = async () => {
- try {
- const data: Product = await apiService.get(`/Product/${id}`);
- setProduct(data);
- } catch (error) {
- console.error("Error fetching product:", error);
- } finally {
- setLoading(false);
- }
- };
+ fetchCategories();
+ fetchBrands();
fetchProduct();
}, [id]);
+
+ useEffect(() => {
+ if (product?.categoryId) {
+ fetchCategoryAttributes(product.categoryId);
+ }
+ }, [product?.categoryId]);
+
+ useEffect(() => {
+ if (product && categories.length > 0 && !selectedRootCategoryId) {
+ const productCategory = categories.find(
+ (c) => c.id === product.categoryId
+ );
+ if (productCategory) {
+ const rootId = productCategory.parentCategoryId || product.categoryId;
+ setSelectedRootCategoryId(rootId);
+ }
+ }
+ }, [product, categories, selectedRootCategoryId]);
+
+ const fetchCategories = async () => {
+ try {
+ const data = await apiService.get("/Category");
+ setCategories(data);
+ } catch (error) {
+ console.error("Error fetching categories:", error);
+ }
+ };
+
+ const fetchBrands = async () => {
+ try {
+ const data = await apiService.get("/Brand");
+ setBrands(data);
+ } catch (error) {
+ console.error("Error fetching brands:", error);
+ }
+ };
+
+ const fetchProduct = async () => {
+ try {
+ const data: Product = await apiService.get(`/Product/${id}`);
+ setProduct(data);
+ setSelectedAttributes(data.attributeValues || []);
+ } catch (error) {
+ console.error("Error fetching product:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchCategoryAttributes = async (categoryId: number) => {
+ try {
+ const data = await apiService.get(
+ `/ProductAttribute/category/${categoryId}`
+ );
+ setCategoryAttributes(data);
+ } catch (error) {
+ console.error("Error fetching category attributes:", error);
+ }
+ };
+
const getProductImageUrl = (product: Product) => {
const primaryPhoto =
product.photos?.find((p) => p.isPrimary) || product.photos?.[0];
@@ -91,12 +197,86 @@ const ProductDetailAdminPage = () => {
: product.imageUrl || "";
};
+ const addAttributeVariant = (attributeId: number) => {
+ setSelectedAttributes([
+ ...selectedAttributes,
+ {
+ attributeId,
+ value: "",
+ price: undefined,
+ stockAmount: undefined,
+ },
+ ]);
+ };
+
+ const removeAttributeVariant = (index: number) => {
+ setSelectedAttributes(selectedAttributes.filter((_, i) => i !== index));
+ };
+
+ const updateAttributeVariant = (index: number, field: string, value: any) => {
+ const updated = [...selectedAttributes];
+ updated[index] = { ...updated[index], [field]: value };
+ setSelectedAttributes(updated);
+ };
+
const handleUpdate = async () => {
if (!product) return;
+
+ const missingRequiredAttributes = categoryAttributes
+ .filter((attr) => attr.isRequired)
+ .filter(
+ (attr) =>
+ !selectedAttributes.some(
+ (sa) => sa.attributeId === attr.id && sa.value
+ )
+ );
+
+ if (missingRequiredAttributes.length > 0) {
+ setMessage(
+ `Please fill in required attributes: ${missingRequiredAttributes
+ .map((a) => a.name)
+ .join(", ")}`
+ );
+ return;
+ }
+
+ const emptyValues = selectedAttributes.filter((attr) => !attr.value);
+ if (emptyValues.length > 0) {
+ setMessage(
+ "Please fill in all attribute values or remove empty entries."
+ );
+ return;
+ }
+
setSaving(true);
try {
- await apiService.put(`/Product/${product.id}`, product);
+ const updateData = {
+ ...product,
+ attributeValues: selectedAttributes.map((attr) => ({
+ attributeId: attr.attributeId,
+ value: attr.value,
+ price: attr.price || null,
+ stockAmount: attr.stockAmount || null,
+ })),
+ };
+
+ await apiService.put(`/Product/${product.id}`, updateData);
+
+ if (photos.length > 0) {
+ for (let i = 0; i < photos.length; i++) {
+ const photoFormData = new FormData();
+ photoFormData.append("ProductId", product.id.toString());
+ photoFormData.append("IsPrimary", (i === 0).toString());
+ photoFormData.append("DisplayOrder", i.toString());
+ photoFormData.append("AltText", product.title);
+ photoFormData.append("File", photos[i]);
+
+ await apiService.uploadFile("/Photo/upload", photoFormData);
+ }
+ }
+
setMessage("Product updated successfully!");
+ fetchProduct();
} catch (error) {
console.error("Error updating product:", error);
setMessage("Failed to update product.");
@@ -105,26 +285,26 @@ const ProductDetailAdminPage = () => {
}
};
-const handleDelete = async () => {
- if (!product) return;
- if (window.confirm("Are you sure you want to delete this product?")) {
- try {
- await apiService.delete(`/Product/${product.id}`);
- navigate("/admin/all-products");
- setMessage("Product deleted successfully!");
- } catch (error: any) {
- console.error("Error deleting product:", error);
-
- const errorMessage = error?.response?.data?.message;
-
- if (errorMessage) {
- setMessage(errorMessage);
- } else {
- setMessage("Failed to delete product. Please try again.");
+ const handleDelete = async () => {
+ if (!product) return;
+ if (window.confirm("Are you sure you want to delete this product?")) {
+ try {
+ await apiService.delete(`/Product/${product.id}`);
+ navigate("/admin/all-products");
+ setMessage("Product deleted successfully!");
+ } catch (error: any) {
+ console.error("Error deleting product:", error);
+
+ const errorMessage = error?.response?.data?.message;
+
+ if (errorMessage) {
+ setMessage(errorMessage);
+ } else {
+ setMessage("Failed to delete product. Please try again.");
+ }
}
}
- }
-};
+ };
if (loading) {
return (
@@ -138,41 +318,81 @@ const handleDelete = async () => {
return Product not found
;
}
+ const attributesByType = categoryAttributes.reduce((acc, attr) => {
+ if (!acc[attr.id]) {
+ acc[attr.id] = {
+ attribute: attr,
+ values: selectedAttributes.filter((sa) => sa.attributeId === attr.id),
+ };
+ }
+ return acc;
+ }, {} as Record);
+
return (
-
-
);