Integrated stock reduction logic and branding updates

This commit is contained in:
2026-04-16 09:11:49 +05:30
parent 3d6a57593d
commit 54172f6dff
16 changed files with 187 additions and 102 deletions

View File

@@ -40,6 +40,7 @@ public class OrderController {
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String status,
@RequestParam(required = false) String paymentType,
@RequestParam(required = false) String orderType,
@RequestParam(required = false) String search,
@RequestParam(defaultValue = "false") boolean archived,
@RequestParam(defaultValue = "0") int page,
@@ -66,6 +67,9 @@ public class OrderController {
if (paymentType != null && !paymentType.isEmpty()) {
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()) {
String searchLower = "%" + search.toLowerCase() + "%";
Join<Order, User> userJoin = root.join("user", JoinType.LEFT);
@@ -110,53 +114,61 @@ public class OrderController {
@PostMapping
public ResponseEntity<Map<String, Object>> placeOrder(@RequestBody Order order) {
// Link items to the order for bidirectional relationship
if (order.getItems() != null) {
for (OrderItem item : order.getItems()) {
System.out.println("🛒 RECEIVED ITEM: " + item.getProductName() + " | StallID: " + item.getStallId() + " | StallName: " + item.getStallName());
item.setOrder(order);
}
}
// Use the actual creation time or current time for counting
LocalDateTime now = LocalDateTime.now();
order.setCreatedAt(now);
// Calculate start of current day to find how many orders placed today
LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();
long todaysOrderCount = orderRepository.countByCreatedAtGreaterThanEqual(startOfDay);
// Generate formatted display ID (#001, #002...) resetting daily
String displayId = String.format("%03d", todaysOrderCount + 1);
order.setDisplayOrderId(displayId);
// Save the complete order first
Order savedOrder = orderRepository.save(order);
// --- Reduct Stock Logic ---
if (savedOrder.getItems() != null) {
for (OrderItem item : savedOrder.getItems()) {
Long productId = item.getProductId();
if (productId != null) {
productRepository.findById(productId).ifPresent(product -> {
int currentStock = product.getStock() != null ? product.getStock() : 0;
product.setStock(currentStock - item.getQuantity());
productRepository.save(product);
System.out.println("Updating Stock for " + product.getName() + ": " + currentStock + " -> " + product.getStock());
});
try {
// Link items to the order for bidirectional relationship
if (order.getItems() != null) {
for (OrderItem item : order.getItems()) {
System.out.println("🛒 RECEIVED ITEM: " + item.getProductName());
item.setOrder(order);
}
}
// Use the actual creation time or current time for counting
LocalDateTime now = LocalDateTime.now();
order.setCreatedAt(now);
// Calculate start of current day to find how many orders placed today
LocalDateTime startOfDay = now.toLocalDate().atStartOfDay();
long todaysOrderCount = orderRepository.countByCreatedAtGreaterThanEqual(startOfDay);
// Generate formatted display ID (#001, #002...) resetting daily
String displayId = String.format("%03d", todaysOrderCount + 1);
order.setDisplayOrderId(displayId);
// Save the complete order first
Order savedOrder = orderRepository.save(order);
// --- Reduct Stock Logic ---
if (savedOrder.getItems() != null) {
for (OrderItem item : savedOrder.getItems()) {
Long productId = item.getProductId();
if (productId != null) {
productRepository.findById(productId).ifPresent(product -> {
int currentStock = product.getStock() != null ? product.getStock() : 0;
product.setStock(currentStock - item.getQuantity());
productRepository.save(product);
System.out.println("Updating Stock for " + product.getName() + ": " + currentStock + " -> " + product.getStock());
});
}
}
}
System.out.println("Placed Daily Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("orderNumber", savedOrder.getOrderNumber());
response.put("displayOrderId", savedOrder.getDisplayOrderId());
response.put("message", "Order placed successfully");
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);
}
System.out.println("Placed Daily Order: " + savedOrder.getId() + " -> Display ID: #" + displayId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("orderNumber", savedOrder.getOrderNumber()); // Secure ID for QR
response.put("displayOrderId", savedOrder.getDisplayOrderId()); // Sequential ID (#001)
response.put("message", "Order placed successfully");
return ResponseEntity.ok(response);
}
@GetMapping("/user/{userId}")

View File

@@ -16,6 +16,16 @@ public class ProductController {
@Autowired
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
public org.springframework.data.domain.Page<Product> getAllProducts(
@RequestParam(defaultValue = "0") int page,

View File

@@ -41,6 +41,9 @@ public class Order {
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private String orderType = "STORE_ORDER"; // STORE_ORDER, QR_ORDER, MY_ORDER, etc.
@Column(name = "is_archived", nullable = false)
private boolean isArchived = false;
@@ -97,6 +100,9 @@ public class Order {
public boolean isArchived() { return isArchived; }
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 void setItems(List<OrderItem> items) { this.items = items; }
}

View File

@@ -76,7 +76,8 @@ const BaseMenu = () => {
setShowProductsModal(true);
setProductsLoading(true);
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();
setAssociatedProducts(data);
} catch (error) {
@@ -88,9 +89,10 @@ const BaseMenu = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const host = window.location.hostname;
const url = editingItem
? `http://localhost:8080/api/base-items/${editingItem.id}`
: 'http://localhost:8080/api/base-items';
? `http://${host}:8080/api/base-items/${editingItem.id}`
: `http://${host}:8080/api/base-items`;
const method = editingItem ? 'PUT' : 'POST';
try {
@@ -119,7 +121,8 @@ const BaseMenu = () => {
const handleToggleActive = async (item: BaseItem) => {
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...item, active: !item.active }),
@@ -200,7 +203,7 @@ const BaseMenu = () => {
</div>
</td>
</tr>
) : items.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 ? (
) : items.filter(item => (item.name || '').toLowerCase().includes((searchTerm || '').toLowerCase())).length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-12 text-center text-[#64748b]">
<p className="text-lg font-medium mb-1">No items found</p>
@@ -209,7 +212,7 @@ const BaseMenu = () => {
</tr>
) : (
items
.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()))
.filter(item => (item.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()))
.map((item) => (
<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>

View File

@@ -74,8 +74,9 @@ const Bills: React.FC = () => {
};
const filteredOrders = orders.filter(order => {
const matchesSearch = order.purchaseId.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.referenceId?.toLowerCase().includes(searchTerm.toLowerCase());
const search = (searchTerm || '').toLowerCase();
const matchesSearch = (order.purchaseId || '').toLowerCase().includes(search) ||
(order.referenceId || '').toLowerCase().includes(search);
const matchesStatus = statusFilter === 'All' || order.status === statusFilter;
return matchesSearch && matchesStatus;
});

View File

@@ -146,10 +146,12 @@ const Customers: React.FC = () => {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
};
const filteredUsers = users.filter(user =>
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.mobileNumber?.includes(searchTerm)
);
const filteredCustomers = customers.filter(user => {
const search = (searchTerm || '').toLowerCase();
const name = (user.name || '').toLowerCase();
return name.includes(search) ||
(user.mobileNumber || '').includes(searchTerm);
});
return (
<>

View File

@@ -43,13 +43,13 @@ const Login = () => {
const user = await response.json();
// Strict Role Validation
if (user.role.toLowerCase() !== role) {
alert(`Access Denied: You are trying to login as ${role.toUpperCase()}, but your credentials belong to a ${user.role.toUpperCase()} account.`);
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.`);
return;
}
sessionStorage.setItem('isLoggedIn', 'true');
sessionStorage.setItem('userRole', user.role.toLowerCase());
sessionStorage.setItem('userRole', (user.role || '').toLowerCase());
sessionStorage.setItem('userPermissions', JSON.stringify(user.permissions || []));
navigate('/store-dashboard');
} else {

View File

@@ -193,7 +193,11 @@ const Managers = () => {
</tr>
</thead>
<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">
<td className="px-6 py-5">
<div className="flex items-center gap-3">

View File

@@ -82,10 +82,12 @@ const Orders: React.FC = () => {
const fetchProducts = async () => {
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) {
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) {
console.error('Error fetching products:', error);
@@ -233,10 +235,11 @@ const Orders: React.FC = () => {
};
const filteredProducts = useMemo(() => {
if (!editSearchQuery.trim()) return [];
const query = (editSearchQuery || '').trim().toLowerCase();
if (!query) return [];
return allProducts.filter(p =>
p.name.toLowerCase().includes(editSearchQuery.toLowerCase()) ||
p.category.toLowerCase().includes(editSearchQuery.toLowerCase())
(p.name || '').toLowerCase().includes(query) ||
(p.category || '').toLowerCase().includes(query)
).slice(0, 5); // Limit results
}, [allProducts, editSearchQuery]);
@@ -246,7 +249,7 @@ const Orders: React.FC = () => {
};
const getStatusColor = (status: string) => {
switch (status.toUpperCase()) {
switch ((status || '').toUpperCase()) {
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 'PENDING': return 'bg-amber-50 text-amber-600 border-amber-100';

View File

@@ -117,15 +117,21 @@ const Products = () => {
const host = window.location.hostname;
const response = await fetch(`http://${host}:8080/api/products?page=${currentPage}&size=${pageSize}`);
const data = await response.json();
if (data && data.content) {
setProducts(data.content);
setTotalElements(data.totalElements);
} else if (Array.isArray(data)) {
setProducts(data);
setTotalElements(data.length);
} else {
setProducts([]);
setTotalElements(0);
}
} catch (error) {
console.error('Error fetching products:', error);
setProducts([]);
setTotalElements(0);
} finally {
setLoading(false);
}
@@ -133,17 +139,26 @@ const Products = () => {
const fetchBaseItems = async () => {
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();
setBaseItems(data);
if (data && data.content) {
setBaseItems(data.content);
} else if (Array.isArray(data)) {
setBaseItems(data);
} else {
setBaseItems([]);
}
} catch (error) {
console.error('Error fetching base items:', error);
setBaseItems([]);
}
};
const fetchAllStalls = async () => {
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();
setAllStalls(data);
} catch (error) {
@@ -153,9 +168,10 @@ const Products = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const host = window.location.hostname;
const url = editingProduct
? `http://localhost:8080/api/products/${editingProduct.id}`
: 'http://localhost:8080/api/products';
? `http://${host}:8080/api/products/${editingProduct.id}`
: `http://${host}:8080/api/products`;
const method = editingProduct ? 'PUT' : 'POST';
try {
@@ -168,6 +184,7 @@ const Products = () => {
setShowModal(false);
setEditingProduct(null);
setFormData(emptyProduct);
setSearchTerm(''); // Clear search term to ensure updated product is visible
fetchProducts();
}
} catch (error) {
@@ -187,7 +204,8 @@ const Products = () => {
const handleToggleActive = async (product: Product) => {
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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...product, active: !product.active }),
@@ -203,7 +221,8 @@ const Products = () => {
const handleToggleStock = async (product: Product) => {
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',
});
if (response.ok) {
@@ -218,7 +237,8 @@ const Products = () => {
const handleDelete = async (product: Product) => {
if (!window.confirm(`Are you sure you want to delete ${product.name}?`)) return;
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',
});
if (response.ok) {
@@ -303,9 +323,21 @@ const Products = () => {
<tbody className="divide-y divide-[#e2e8f0]">
{loading ? (
<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>
) : 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">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
@@ -327,9 +359,9 @@ const Products = () => {
<div className="flex flex-wrap gap-1">
{(() => {
// Combine direct stalls with stalls that have this product's category in their baseItems
const directStalls = product.stalls || [];
const indirectStalls = allStalls.filter(s =>
s.baseItems?.some(bi => bi.name.toLowerCase() === product.category?.toLowerCase())
const directStalls = (product && product.stalls) || [];
const indirectStalls = (allStalls || []).filter(s =>
s.baseItems?.some(bi => bi.name?.toLowerCase() === product.category?.toLowerCase())
).map(s => ({ id: s.id, name: s.name }));
// Unique stalls by ID

View File

@@ -161,7 +161,7 @@ const PurchaseAnalytics = () => {
};
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;
return matchesSearch && matchesFilter;
});

View File

@@ -258,7 +258,7 @@ const Purchases: React.FC = () => {
<Loader2 className="animate-spin inline-block mr-2" /> Loading records...
</td>
</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">
<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>

View File

@@ -109,10 +109,11 @@ const Staff = () => {
}
};
const filteredStaff = staffList.filter(s =>
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.email.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredStaff = staff.filter(s => {
const query = (searchQuery || '').toLowerCase();
return (s.name || '').toLowerCase().includes(query) ||
(s.email || '').toLowerCase().includes(query);
});
return (
<div className="p-8 space-y-8 bg-[#f8fafc] min-h-screen font-inter">

View File

@@ -198,8 +198,15 @@ const Stalls: React.FC = () => {
fetch(`http://${host}:8080/api/base-items`)
]);
if (prodRes.ok) setAllProducts(await prodRes.json());
if (baseRes.ok) setAllBaseItems(await baseRes.json());
if (prodRes.ok) {
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) {
console.error('Error fetching available items:', error);
}
@@ -234,10 +241,11 @@ const Stalls: React.FC = () => {
}
};
const filteredStalls = stalls.filter(s =>
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.description.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredStalls = stalls.filter(s => {
const query = (searchQuery || '').toLowerCase();
return (s.name || '').toLowerCase().includes(query) ||
(s.description || '').toLowerCase().includes(query);
});
const isStallOpen = (stall: Stall) => {
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">
{activeTab === 'products' ? (
allProducts
.filter(p => p.name.toLowerCase().includes(itemSearchQuery.toLowerCase()))
.filter(p => (p.name || '').toLowerCase().includes((itemSearchQuery || '').toLowerCase()))
.map(product => {
const isSelected = tempProductIds.includes(product.id);
return (
@@ -644,7 +652,7 @@ const Stalls: React.FC = () => {
})
) : (
allBaseItems
.filter(b => b.name.toLowerCase().includes(itemSearchQuery.toLowerCase()))
.filter(b => (b.name || '').toLowerCase().includes((itemSearchQuery || '').toLowerCase()))
.map(baseItem => {
const isSelected = tempBaseItemIds.includes(baseItem.id);
return (

View File

@@ -60,11 +60,11 @@ const Terminals = () => {
}
};
const filteredTerminals = terminals.filter(t =>
t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
t.location.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredTerminals = terminals.filter(t => {
const query = (searchQuery || '').toLowerCase();
return (t.name || '').toLowerCase().includes(query) ||
(t.location || '').toLowerCase().includes(query);
});
return (
<div className="p-8 max-w-7xl mx-auto space-y-8 font-inter">
{/* Header Section */}

View File

@@ -134,10 +134,13 @@ const Vendors: React.FC = () => {
}
};
const filteredVendors = vendors.filter(v =>
v.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
v.companyName?.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredVendors = vendors.filter(v => {
if (!v) return false;
const search = (searchTerm || '').toLowerCase();
const name = (v.name || '').toLowerCase();
const company = (v.companyName || '').toLowerCase();
return name.includes(search) || company.includes(search);
});
return (
<>