Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions .idea/.idea.ATSYN/.idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 32 additions & 31 deletions ATSYN.Api/Controllers/ProductController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -542,44 +542,45 @@ public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto updateDt
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
try
var product = await _context.Products.FindAsync(id);
if (product == null)
{
var product = await _context.Products.FindAsync(id);
return NotFound($"Product with ID {id} not found.");
}

if (product == null)
{
return NotFound();
}
var activeOrderStatuses = new[]
{
OrderStatus.Pending,
OrderStatus.Confirmed,
OrderStatus.Processing,
OrderStatus.Shipped
};

var hasOrderItems = await _context.OrderItems
.AnyAsync(oi => oi.ProductId == id);
var hasActiveOrders = await _context.OrderItems
.Include(oi => oi.Order)
.AnyAsync(oi => oi.ProductId == id && activeOrderStatuses.Contains(oi.Order.Status));

if (hasOrderItems)
{
return Conflict(new
{
message = "Cannot delete this product because it exists in customer order history."
});
}
if (hasActiveOrders)
{
return BadRequest(new
{
message = "Cannot delete product with active orders. Please wait until orders are delivered or cancelled.",
canDelete = false
});
}

_context.Products.Remove(product);
await _context.SaveChangesAsync();
var hasAnyOrders = await _context.OrderItems.AnyAsync(oi => oi.ProductId == id);

return NoContent();
}
catch (DbUpdateException ex)
{
if (ex.InnerException?.Message.Contains("FK_OrderItems_Products") == true ||
ex.InnerException?.Message.Contains("REFERENCE constraint") == true)
{
return Conflict(new
{
message = "Cannot delete this product because it exists in customer order history."
});
}
_context.Products.Remove(product);
await _context.SaveChangesAsync();

return StatusCode(500, new { message = "An error occurred while deleting the product." });
}
return Ok(new
{
message = hasAnyOrders
? "Product deleted successfully (had completed/cancelled orders)"
: "Product deleted successfully",
hadOrders = hasAnyOrders
});
}

private async Task<bool> ProductExists(int id)
Expand Down
6 changes: 3 additions & 3 deletions ATSYN.Data/Data/Entities/Orders/OrderItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public void Configure(EntityTypeBuilder<OrderItem> builder)
builder.Property(oi => oi.Quantity)
.IsRequired();

// Relationships
builder.HasOne(oi => oi.Order)
.WithMany(o => o.OrderItems)
.HasForeignKey(oi => oi.OrderId)
Expand All @@ -53,8 +52,9 @@ public void Configure(EntityTypeBuilder<OrderItem> builder)
builder.HasOne(oi => oi.Product)
.WithMany()
.HasForeignKey(oi => oi.ProductId)
.OnDelete(DeleteBehavior.Restrict);
.OnDelete(DeleteBehavior.SetNull);

builder.ToTable("OrderItems");
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useState } from "react";
import { Select, Group, TextInput, Button } from "@mantine/core";

interface Brand {
id: number;
name: string;
}

interface BrandSelectProps {
brands: Brand[];
value: number | null;
onBrandChange: (brand: Brand | null) => void;
onBrandCreate?: (brandName: string) => Promise<Brand>;
}

export const BrandSelect: React.FC<BrandSelectProps> = ({
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 (
<div>
<Select
label="Brand"
placeholder="Select brand"
data={brands.map((brand) => ({
value: brand.id.toString(),
label: brand.name,
}))}
value={value?.toString() || null}
onChange={(val) => {
if (val) {
const brand = brands.find((b) => b.id.toString() === val);
onBrandChange(brand || null);
} else {
onBrandChange(null);
}
}}
searchable
clearable
nothingFoundMessage="Type to search brands"
/>
{onBrandCreate && (
<Group gap="xs" mt="xs">
<TextInput
placeholder="Or create new brand"
value={newBrandName}
onChange={(e) => setNewBrandName(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleCreateBrand();
}
}}
style={{ flex: 1 }}
/>
<Button
onClick={handleCreateBrand}
loading={creating}
disabled={!newBrandName.trim()}
>
Create
</Button>
</Group>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
Text,
Stack,
Table,
Badge,
SegmentedControl,
} from "@mantine/core";
import { apiService } from "../../../../config/api";

Expand Down Expand Up @@ -46,9 +48,18 @@ const AllProducts = () => {
const [Products, setProducts] = useState<Product[]>([]);
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");
Expand All @@ -73,6 +84,7 @@ const AllProducts = () => {
hasFetched.current = true;
}
}, []);

const getProductImageUrl = (product: Product) => {
const primaryPhoto =
product.photos?.find((p) => p.isPrimary) || product.photos?.[0];
Expand Down Expand Up @@ -106,13 +118,33 @@ const AllProducts = () => {
</Group>
</Group>

<Group mb="md">
<SegmentedControl
value={visibilityFilter}
onChange={(value) =>
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",
},
]}
/>
</Group>

{view === "card" ? (
<SimpleGrid
cols={{ base: 1, sm: 2, md: 3, lg: 4 }}
spacing="lg"
className="products-grid"
>
{Products.map((p) => (
{filteredProducts.map((p) => (
<Card
key={p.id}
shadow="sm"
Expand All @@ -136,7 +168,14 @@ const AllProducts = () => {
</Card.Section>

<Stack gap={4} mt="sm">
<Text fw={600}>{p.title}</Text>
<Group justify="space-between" align="flex-start">
<Text fw={600}>{p.title}</Text>
{!p.isVisible && (
<Badge color="red" size="sm" variant="filled">
Hidden
</Badge>
)}
</Group>
<Text c="#8a00c4" fw={500}>
${p.price.toFixed(2)}
</Text>
Expand Down Expand Up @@ -164,19 +203,35 @@ const AllProducts = () => {
>
<Table.Thead>
<Table.Tr color="#8a00c4">
<Table.Th bg = "#8a00c4">Name</Table.Th>
<Table.Th ta="right" bg = "#8a00c4">Price</Table.Th>
<Table.Th ta="right" bg = "#8a00c4">Stock</Table.Th>
<Table.Th bg="#8a00c4">Name</Table.Th>
<Table.Th bg="#8a00c4">Visibility</Table.Th>
<Table.Th ta="right" bg="#8a00c4">
Price
</Table.Th>
<Table.Th ta="right" bg="#8a00c4">
Stock
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{Products.map((p) => (
{filteredProducts.map((p) => (
<Table.Tr
key={p.id}
onClick={() => navigate(`/admin/products/${p.id}`)}
style={{ cursor: "pointer" }}
>
<Table.Td fw={500}>{p.title}</Table.Td>
<Table.Td>
{p.isVisible ? (
<Badge color="green" size="sm" variant="light">
Visible
</Badge>
) : (
<Badge color="red" size="sm" variant="filled">
Hidden
</Badge>
)}
</Table.Td>
<Table.Td ta="right">${p.price.toFixed(2)}</Table.Td>
<Table.Td ta="right">
<Text c={p.stockAmount > 0 ? "green" : "red"}>
Expand All @@ -190,6 +245,12 @@ const AllProducts = () => {
</Table.Tbody>
</Table>
)}

{filteredProducts.length === 0 && (
<Text ta="center" c="dimmed" mt="xl">
No products found for the selected filter.
</Text>
)}
</Container>
);
};
Expand Down
Loading