Integrated stock reduction logic and branding updates
This commit is contained in:
@@ -40,6 +40,7 @@ public class OrderController {
|
|||||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
|
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
|
||||||
@RequestParam(required = false) String status,
|
@RequestParam(required = false) String status,
|
||||||
@RequestParam(required = false) String paymentType,
|
@RequestParam(required = false) String paymentType,
|
||||||
|
@RequestParam(required = false) String orderType,
|
||||||
@RequestParam(required = false) String search,
|
@RequestParam(required = false) String search,
|
||||||
@RequestParam(defaultValue = "false") boolean archived,
|
@RequestParam(defaultValue = "false") boolean archived,
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@@ -66,6 +67,9 @@ public class OrderController {
|
|||||||
if (paymentType != null && !paymentType.isEmpty()) {
|
if (paymentType != null && !paymentType.isEmpty()) {
|
||||||
predicates.add(cb.equal(root.get("paymentMethod"), paymentType));
|
predicates.add(cb.equal(root.get("paymentMethod"), paymentType));
|
||||||
}
|
}
|
||||||
|
if (orderType != null && !orderType.isEmpty()) {
|
||||||
|
predicates.add(cb.equal(root.get("orderType"), orderType));
|
||||||
|
}
|
||||||
if (search != null && !search.isEmpty()) {
|
if (search != null && !search.isEmpty()) {
|
||||||
String searchLower = "%" + search.toLowerCase() + "%";
|
String searchLower = "%" + search.toLowerCase() + "%";
|
||||||
Join<Order, User> userJoin = root.join("user", JoinType.LEFT);
|
Join<Order, User> userJoin = root.join("user", JoinType.LEFT);
|
||||||
@@ -110,10 +114,11 @@ public class OrderController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Map<String, Object>> placeOrder(@RequestBody Order order) {
|
public ResponseEntity<Map<String, Object>> placeOrder(@RequestBody Order order) {
|
||||||
|
try {
|
||||||
// Link items to the order for bidirectional relationship
|
// Link items to the order for bidirectional relationship
|
||||||
if (order.getItems() != null) {
|
if (order.getItems() != null) {
|
||||||
for (OrderItem item : order.getItems()) {
|
for (OrderItem item : order.getItems()) {
|
||||||
System.out.println("🛒 RECEIVED ITEM: " + item.getProductName() + " | StallID: " + item.getStallId() + " | StallName: " + item.getStallName());
|
System.out.println("🛒 RECEIVED ITEM: " + item.getProductName());
|
||||||
item.setOrder(order);
|
item.setOrder(order);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,11 +157,18 @@ public class OrderController {
|
|||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("orderNumber", savedOrder.getOrderNumber()); // Secure ID for QR
|
response.put("orderNumber", savedOrder.getOrderNumber());
|
||||||
response.put("displayOrderId", savedOrder.getDisplayOrderId()); // Sequential ID (#001)
|
response.put("displayOrderId", savedOrder.getDisplayOrderId());
|
||||||
response.put("message", "Order placed successfully");
|
response.put("message", "Order placed successfully");
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
Map<String, Object> errorResponse = new HashMap<>();
|
||||||
|
errorResponse.put("success", false);
|
||||||
|
errorResponse.put("error", e.getMessage());
|
||||||
|
return ResponseEntity.status(500).body(errorResponse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/user/{userId}")
|
@GetMapping("/user/{userId}")
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ public class ProductController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ProductRepository productRepository;
|
private ProductRepository productRepository;
|
||||||
|
|
||||||
|
@GetMapping("/categories")
|
||||||
|
public List<String> getAllCategories() {
|
||||||
|
return productRepository.findAll().stream()
|
||||||
|
.map(Product::getCategory)
|
||||||
|
.filter(c -> c != null && !c.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public org.springframework.data.domain.Page<Product> getAllProducts(
|
public org.springframework.data.domain.Page<Product> getAllProducts(
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ public class Order {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String orderType = "STORE_ORDER"; // STORE_ORDER, QR_ORDER, MY_ORDER, etc.
|
||||||
|
|
||||||
@Column(name = "is_archived", nullable = false)
|
@Column(name = "is_archived", nullable = false)
|
||||||
private boolean isArchived = false;
|
private boolean isArchived = false;
|
||||||
|
|
||||||
@@ -97,6 +100,9 @@ public class Order {
|
|||||||
public boolean isArchived() { return isArchived; }
|
public boolean isArchived() { return isArchived; }
|
||||||
public void setArchived(boolean archived) { isArchived = archived; }
|
public void setArchived(boolean archived) { isArchived = archived; }
|
||||||
|
|
||||||
|
public String getOrderType() { return orderType; }
|
||||||
|
public void setOrderType(String orderType) { this.orderType = orderType; }
|
||||||
|
|
||||||
public List<OrderItem> getItems() { return items; }
|
public List<OrderItem> getItems() { return items; }
|
||||||
public void setItems(List<OrderItem> items) { this.items = items; }
|
public void setItems(List<OrderItem> items) { this.items = items; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ const BaseMenu = () => {
|
|||||||
setShowProductsModal(true);
|
setShowProductsModal(true);
|
||||||
setProductsLoading(true);
|
setProductsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/products/category/${encodeURIComponent(baseItem.name)}`);
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/products/category/${encodeURIComponent(baseItem.name)}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setAssociatedProducts(data);
|
setAssociatedProducts(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,9 +89,10 @@ const BaseMenu = () => {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const host = window.location.hostname;
|
||||||
const url = editingItem
|
const url = editingItem
|
||||||
? `http://localhost:8080/api/base-items/${editingItem.id}`
|
? `http://${host}:8080/api/base-items/${editingItem.id}`
|
||||||
: 'http://localhost:8080/api/base-items';
|
: `http://${host}:8080/api/base-items`;
|
||||||
const method = editingItem ? 'PUT' : 'POST';
|
const method = editingItem ? 'PUT' : 'POST';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,7 +121,8 @@ const BaseMenu = () => {
|
|||||||
|
|
||||||
const handleToggleActive = async (item: BaseItem) => {
|
const handleToggleActive = async (item: BaseItem) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/base-items/${item.id}`, {
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/base-items/${item.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...item, active: !item.active }),
|
body: JSON.stringify({ ...item, active: !item.active }),
|
||||||
@@ -200,7 +203,7 @@ const BaseMenu = () => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : items.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 ? (
|
) : items.filter(item => (item.name || '').toLowerCase().includes((searchTerm || '').toLowerCase())).length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">
|
<td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">
|
||||||
<p className="text-lg font-medium mb-1">No items found</p>
|
<p className="text-lg font-medium mb-1">No items found</p>
|
||||||
@@ -209,7 +212,7 @@ const BaseMenu = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
items
|
items
|
||||||
.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
|
.filter(item => (item.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()))
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<tr key={item.id} className="hover:bg-gray-50/50 transition-all group">
|
<tr key={item.id} className="hover:bg-gray-50/50 transition-all group">
|
||||||
<td className="px-6 py-4 text-sm font-medium text-[#64748b]">#{item.id}</td>
|
<td className="px-6 py-4 text-sm font-medium text-[#64748b]">#{item.id}</td>
|
||||||
|
|||||||
@@ -74,8 +74,9 @@ const Bills: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredOrders = orders.filter(order => {
|
const filteredOrders = orders.filter(order => {
|
||||||
const matchesSearch = order.purchaseId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const search = (searchTerm || '').toLowerCase();
|
||||||
order.referenceId?.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = (order.purchaseId || '').toLowerCase().includes(search) ||
|
||||||
|
(order.referenceId || '').toLowerCase().includes(search);
|
||||||
const matchesStatus = statusFilter === 'All' || order.status === statusFilter;
|
const matchesStatus = statusFilter === 'All' || order.status === statusFilter;
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -146,10 +146,12 @@ const Customers: React.FC = () => {
|
|||||||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredUsers = users.filter(user =>
|
const filteredCustomers = customers.filter(user => {
|
||||||
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const search = (searchTerm || '').toLowerCase();
|
||||||
user.mobileNumber?.includes(searchTerm)
|
const name = (user.name || '').toLowerCase();
|
||||||
);
|
return name.includes(search) ||
|
||||||
|
(user.mobileNumber || '').includes(searchTerm);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ const Login = () => {
|
|||||||
const user = await response.json();
|
const user = await response.json();
|
||||||
|
|
||||||
// Strict Role Validation
|
// Strict Role Validation
|
||||||
if (user.role.toLowerCase() !== role) {
|
if ((user.role || '').toLowerCase() !== (role || '').toLowerCase()) {
|
||||||
alert(`Access Denied: You are trying to login as ${role.toUpperCase()}, but your credentials belong to a ${user.role.toUpperCase()} account.`);
|
alert(`Access Denied: You are trying to login as ${(role || '').toUpperCase()}, but your credentials belong to a ${(user.role || '').toUpperCase()} account.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('isLoggedIn', 'true');
|
sessionStorage.setItem('isLoggedIn', 'true');
|
||||||
sessionStorage.setItem('userRole', user.role.toLowerCase());
|
sessionStorage.setItem('userRole', (user.role || '').toLowerCase());
|
||||||
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
|
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
|
||||||
navigate('/store-dashboard');
|
navigate('/store-dashboard');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -193,7 +193,11 @@ const Managers = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-[#e2e8f0]">
|
<tbody className="divide-y divide-[#e2e8f0]">
|
||||||
{managers.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase())).map((manager) => (
|
{managers.filter(m => {
|
||||||
|
const query = (searchQuery || '').toLowerCase();
|
||||||
|
return (m.name || '').toLowerCase().includes(query) ||
|
||||||
|
(m.email || '').toLowerCase().includes(query);
|
||||||
|
}).map((manager) => (
|
||||||
<tr key={manager.id} className="hover:bg-gray-50/50 transition-colors">
|
<tr key={manager.id} className="hover:bg-gray-50/50 transition-colors">
|
||||||
<td className="px-6 py-5">
|
<td className="px-6 py-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -82,10 +82,12 @@ const Orders: React.FC = () => {
|
|||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://${window.location.hostname}:8080/api/products`);
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/products`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setAllProducts(data);
|
// Handle both Array and Page object responses for robustness
|
||||||
|
setAllProducts(Array.isArray(data) ? data : (data.content || []));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching products:', error);
|
console.error('Error fetching products:', error);
|
||||||
@@ -233,10 +235,11 @@ const Orders: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
if (!editSearchQuery.trim()) return [];
|
const query = (editSearchQuery || '').trim().toLowerCase();
|
||||||
|
if (!query) return [];
|
||||||
return allProducts.filter(p =>
|
return allProducts.filter(p =>
|
||||||
p.name.toLowerCase().includes(editSearchQuery.toLowerCase()) ||
|
(p.name || '').toLowerCase().includes(query) ||
|
||||||
p.category.toLowerCase().includes(editSearchQuery.toLowerCase())
|
(p.category || '').toLowerCase().includes(query)
|
||||||
).slice(0, 5); // Limit results
|
).slice(0, 5); // Limit results
|
||||||
}, [allProducts, editSearchQuery]);
|
}, [allProducts, editSearchQuery]);
|
||||||
|
|
||||||
@@ -246,7 +249,7 @@ const Orders: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status.toUpperCase()) {
|
switch ((status || '').toUpperCase()) {
|
||||||
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
|
case 'COMPLETED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
|
||||||
case 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
|
case 'PAID': return 'bg-indigo-50 text-indigo-600 border-indigo-100';
|
||||||
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100';
|
case 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100';
|
||||||
|
|||||||
@@ -117,15 +117,21 @@ const Products = () => {
|
|||||||
const host = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`);
|
const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data && data.content) {
|
if (data && data.content) {
|
||||||
setProducts(data.content);
|
setProducts(data.content);
|
||||||
setTotalElements(data.totalElements);
|
setTotalElements(data.totalElements);
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
setProducts(data);
|
||||||
|
setTotalElements(data.length);
|
||||||
} else {
|
} else {
|
||||||
setProducts([]);
|
setProducts([]);
|
||||||
setTotalElements(0);
|
setTotalElements(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching products:', error);
|
console.error('Error fetching products:', error);
|
||||||
|
setProducts([]);
|
||||||
|
setTotalElements(0);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -133,17 +139,26 @@ const Products = () => {
|
|||||||
|
|
||||||
const fetchBaseItems = async () => {
|
const fetchBaseItems = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8080/api/base-items');
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/base-items`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
if (data && data.content) {
|
||||||
|
setBaseItems(data.content);
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
setBaseItems(data);
|
setBaseItems(data);
|
||||||
|
} else {
|
||||||
|
setBaseItems([]);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching base items:', error);
|
console.error('Error fetching base items:', error);
|
||||||
|
setBaseItems([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAllStalls = async () => {
|
const fetchAllStalls = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8080/api/stalls');
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/stalls`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setAllStalls(data);
|
setAllStalls(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -153,9 +168,10 @@ const Products = () => {
|
|||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const host = window.location.hostname;
|
||||||
const url = editingProduct
|
const url = editingProduct
|
||||||
? `http://localhost:8080/api/products/${editingProduct.id}`
|
? `http://${host}:8080/api/products/${editingProduct.id}`
|
||||||
: 'http://localhost:8080/api/products';
|
: `http://${host}:8080/api/products`;
|
||||||
const method = editingProduct ? 'PUT' : 'POST';
|
const method = editingProduct ? 'PUT' : 'POST';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -168,6 +184,7 @@ const Products = () => {
|
|||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
setEditingProduct(null);
|
setEditingProduct(null);
|
||||||
setFormData(emptyProduct);
|
setFormData(emptyProduct);
|
||||||
|
setSearchTerm(''); // Clear search term to ensure updated product is visible
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -187,7 +204,8 @@ const Products = () => {
|
|||||||
|
|
||||||
const handleToggleActive = async (product: Product) => {
|
const handleToggleActive = async (product: Product) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}`, {
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/products/${product.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ...product, active: !product.active }),
|
body: JSON.stringify({ ...product, active: !product.active }),
|
||||||
@@ -203,7 +221,8 @@ const Products = () => {
|
|||||||
|
|
||||||
const handleToggleStock = async (product: Product) => {
|
const handleToggleStock = async (product: Product) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}/toggle-stock`, {
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/products/${product.id}/toggle-stock`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -218,7 +237,8 @@ const Products = () => {
|
|||||||
const handleDelete = async (product: Product) => {
|
const handleDelete = async (product: Product) => {
|
||||||
if (!window.confirm(`Are you sure you want to delete ${product.name}?`)) return;
|
if (!window.confirm(`Are you sure you want to delete ${product.name}?`)) return;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/api/products/${product.id}`, {
|
const host = window.location.hostname;
|
||||||
|
const response = await fetch(`http://${host}:8080/api/products/${product.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -303,9 +323,21 @@ const Products = () => {
|
|||||||
<tbody className="divide-y divide-[#e2e8f0]">
|
<tbody className="divide-y divide-[#e2e8f0]">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]"><RefreshCw className="animate-spin inline mr-2" />Loading...</td></tr>
|
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]"><RefreshCw className="animate-spin inline mr-2" />Loading...</td></tr>
|
||||||
) : products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.category.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 ? (
|
) : products.filter(p => {
|
||||||
|
if (!p) return false;
|
||||||
|
const search = (searchTerm || '').toLowerCase();
|
||||||
|
const name = (p.name || '').toLowerCase();
|
||||||
|
const category = (p.category || '').toLowerCase();
|
||||||
|
return name.includes(search) || category.includes(search);
|
||||||
|
}).length === 0 ? (
|
||||||
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">No products found</td></tr>
|
<tr><td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">No products found</td></tr>
|
||||||
) : products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.category.toLowerCase().includes(searchTerm.toLowerCase())).map((product) => (
|
) : products.filter(p => {
|
||||||
|
if (!p) return false;
|
||||||
|
const search = (searchTerm || '').toLowerCase();
|
||||||
|
const name = (p.name || '').toLowerCase();
|
||||||
|
const category = (p.category || '').toLowerCase();
|
||||||
|
return name.includes(search) || category.includes(search);
|
||||||
|
}).map((product) => (
|
||||||
<tr key={product.id} className="hover:bg-gray-50/50 transition-all">
|
<tr key={product.id} className="hover:bg-gray-50/50 transition-all">
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -327,9 +359,9 @@ const Products = () => {
|
|||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{(() => {
|
{(() => {
|
||||||
// Combine direct stalls with stalls that have this product's category in their baseItems
|
// Combine direct stalls with stalls that have this product's category in their baseItems
|
||||||
const directStalls = product.stalls || [];
|
const directStalls = (product && product.stalls) || [];
|
||||||
const indirectStalls = allStalls.filter(s =>
|
const indirectStalls = (allStalls || []).filter(s =>
|
||||||
s.baseItems?.some(bi => bi.name.toLowerCase() === product.category?.toLowerCase())
|
s.baseItems?.some(bi => bi.name?.toLowerCase() === product.category?.toLowerCase())
|
||||||
).map(s => ({ id: s.id, name: s.name }));
|
).map(s => ({ id: s.id, name: s.name }));
|
||||||
|
|
||||||
// Unique stalls by ID
|
// Unique stalls by ID
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ const PurchaseAnalytics = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredAnalytics = analytics.filter(item => {
|
const filteredAnalytics = analytics.filter(item => {
|
||||||
const matchesSearch = item.product.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = (item.product || '').toLowerCase().includes((searchTerm || '').toLowerCase());
|
||||||
const matchesFilter = filterType === 'all' || item.trend === filterType;
|
const matchesFilter = filterType === 'all' || item.trend === filterType;
|
||||||
return matchesSearch && matchesFilter;
|
return matchesSearch && matchesFilter;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ const Purchases: React.FC = () => {
|
|||||||
<Loader2 className="animate-spin inline-block mr-2" /> Loading records...
|
<Loader2 className="animate-spin inline-block mr-2" /> Loading records...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : orders.filter(o => o.purchaseId.toLowerCase().includes(searchTerm.toLowerCase())).map((order) => (
|
) : orders.filter(o => (o.purchaseId || '').toLowerCase().includes((searchTerm || '').toLowerCase())).map((order) => (
|
||||||
<tr key={order.id} className="hover:bg-gray-50/50 transition-all font-medium">
|
<tr key={order.id} className="hover:bg-gray-50/50 transition-all font-medium">
|
||||||
<td className="px-6 py-4 text-sm font-bold text-[#231651]">{order.purchaseId}</td>
|
<td className="px-6 py-4 text-sm font-bold text-[#231651]">{order.purchaseId}</td>
|
||||||
<td className="px-6 py-4 text-sm text-[#64748b]">{new Date(order.date).toLocaleDateString()}</td>
|
<td className="px-6 py-4 text-sm text-[#64748b]">{new Date(order.date).toLocaleDateString()}</td>
|
||||||
|
|||||||
@@ -109,10 +109,11 @@ const Staff = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredStaff = staffList.filter(s =>
|
const filteredStaff = staff.filter(s => {
|
||||||
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const query = (searchQuery || '').toLowerCase();
|
||||||
s.email.toLowerCase().includes(searchQuery.toLowerCase())
|
return (s.name || '').toLowerCase().includes(query) ||
|
||||||
);
|
(s.email || '').toLowerCase().includes(query);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 bg-[#f8fafc] min-h-screen font-inter">
|
<div className="p-8 space-y-8 bg-[#f8fafc] min-h-screen font-inter">
|
||||||
|
|||||||
@@ -198,8 +198,15 @@ const Stalls: React.FC = () => {
|
|||||||
fetch(`http://${host}:8080/api/base-items`)
|
fetch(`http://${host}:8080/api/base-items`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (prodRes.ok) setAllProducts(await prodRes.json());
|
if (prodRes.ok) {
|
||||||
if (baseRes.ok) setAllBaseItems(await baseRes.json());
|
const data = await prodRes.json();
|
||||||
|
// Handle both Array and Page object responses for robustness
|
||||||
|
setAllProducts(Array.isArray(data) ? data : (data.content || []));
|
||||||
|
}
|
||||||
|
if (baseRes.ok) {
|
||||||
|
const data = await baseRes.json();
|
||||||
|
setAllBaseItems(Array.isArray(data) ? data : (data.content || []));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching available items:', error);
|
console.error('Error fetching available items:', error);
|
||||||
}
|
}
|
||||||
@@ -234,10 +241,11 @@ const Stalls: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredStalls = stalls.filter(s =>
|
const filteredStalls = stalls.filter(s => {
|
||||||
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const query = (searchQuery || '').toLowerCase();
|
||||||
s.description.toLowerCase().includes(searchQuery.toLowerCase())
|
return (s.name || '').toLowerCase().includes(query) ||
|
||||||
);
|
(s.description || '').toLowerCase().includes(query);
|
||||||
|
});
|
||||||
|
|
||||||
const isStallOpen = (stall: Stall) => {
|
const isStallOpen = (stall: Stall) => {
|
||||||
if (stall.temporarilyClosed) return false;
|
if (stall.temporarilyClosed) return false;
|
||||||
@@ -618,7 +626,7 @@ const Stalls: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4">
|
||||||
{activeTab === 'products' ? (
|
{activeTab === 'products' ? (
|
||||||
allProducts
|
allProducts
|
||||||
.filter(p => p.name.toLowerCase().includes(itemSearchQuery.toLowerCase()))
|
.filter(p => (p.name || '').toLowerCase().includes((itemSearchQuery || '').toLowerCase()))
|
||||||
.map(product => {
|
.map(product => {
|
||||||
const isSelected = tempProductIds.includes(product.id);
|
const isSelected = tempProductIds.includes(product.id);
|
||||||
return (
|
return (
|
||||||
@@ -644,7 +652,7 @@ const Stalls: React.FC = () => {
|
|||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
allBaseItems
|
allBaseItems
|
||||||
.filter(b => b.name.toLowerCase().includes(itemSearchQuery.toLowerCase()))
|
.filter(b => (b.name || '').toLowerCase().includes((itemSearchQuery || '').toLowerCase()))
|
||||||
.map(baseItem => {
|
.map(baseItem => {
|
||||||
const isSelected = tempBaseItemIds.includes(baseItem.id);
|
const isSelected = tempBaseItemIds.includes(baseItem.id);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ const Terminals = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredTerminals = terminals.filter(t =>
|
const filteredTerminals = terminals.filter(t => {
|
||||||
t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const query = (searchQuery || '').toLowerCase();
|
||||||
t.location.toLowerCase().includes(searchQuery.toLowerCase())
|
return (t.name || '').toLowerCase().includes(query) ||
|
||||||
);
|
(t.location || '').toLowerCase().includes(query);
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-7xl mx-auto space-y-8 font-inter">
|
<div className="p-8 max-w-7xl mx-auto space-y-8 font-inter">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
|
|||||||
@@ -134,10 +134,13 @@ const Vendors: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredVendors = vendors.filter(v =>
|
const filteredVendors = vendors.filter(v => {
|
||||||
v.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
if (!v) return false;
|
||||||
v.companyName?.toLowerCase().includes(searchTerm.toLowerCase())
|
const search = (searchTerm || '').toLowerCase();
|
||||||
);
|
const name = (v.name || '').toLowerCase();
|
||||||
|
const company = (v.companyName || '').toLowerCase();
|
||||||
|
return name.includes(search) || company.includes(search);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user