Skip to content

Commit a96f071

Browse files
committed
fix: Synchronize the input price on the price settings page and add a notification
1 parent 28be6bf commit a96f071

File tree

8 files changed

+189
-55
lines changed

8 files changed

+189
-55
lines changed

backend/src/main/java/com/smalltrend/dto/inventory/purchaseorder/PurchaseOrderResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class PurchaseOrderResponse {
5151

5252
private String notes;
5353
private String rejectionReason;
54+
private Integer syncedPurchasePriceCount;
5455

5556
// Items
5657
private List<PurchaseOrderItemResponse> items;

backend/src/main/java/com/smalltrend/repository/ProductBatchRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import java.time.LocalDate;
1010
import java.util.List;
11+
import java.util.Optional;
1112

1213
@Repository
1314
public interface ProductBatchRepository extends JpaRepository<ProductBatch, Integer> {
@@ -29,4 +30,6 @@ List<ProductBatch> findExpiringSoonBatches(@Param("today") LocalDate today,
2930
@Param("futureDate") LocalDate futureDate);
3031

3132
List<ProductBatch> findByVariantId(Integer variantId);
33+
34+
Optional<ProductBatch> findFirstByVariantIdOrderByIdDesc(Integer variantId);
3235
}

backend/src/main/java/com/smalltrend/service/VariantPriceService.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
import org.springframework.stereotype.Service;
1313
import org.springframework.transaction.annotation.Transactional;
1414

15+
import java.math.BigDecimal;
1516
import java.time.LocalDate;
1617
import java.time.temporal.ChronoUnit;
1718
import java.util.List;
1819
import java.util.stream.Collectors;
1920

21+
import static com.smalltrend.entity.enums.VariantPriceStatus.ACTIVE;
22+
2023
@Service
2124
@RequiredArgsConstructor
2225
public class VariantPriceService {
@@ -43,7 +46,7 @@ public VariantPriceResponse createPrice(Integer variantId, VariantPriceRequest r
4346

4447
// Deactivate all current ACTIVE prices for this variant
4548
List<VariantPrice> activePrices = variantPriceRepository.findByVariantIdAndStatus(
46-
variantId, VariantPriceStatus.ACTIVE);
49+
variantId, ACTIVE);
4750
for (VariantPrice activePrice : activePrices) {
4851
activePrice.setStatus(VariantPriceStatus.INACTIVE);
4952
variantPriceRepository.save(activePrice);
@@ -83,11 +86,29 @@ public List<VariantPriceResponse> getPriceHistory(Integer variantId) {
8386
* Lấy giá đang ACTIVE của variant.
8487
*/
8588
public VariantPriceResponse getActivePrice(Integer variantId) {
86-
return variantPriceRepository.findFirstByVariantIdAndStatus(variantId, VariantPriceStatus.ACTIVE)
89+
return variantPriceRepository.findFirstByVariantIdAndStatus(variantId, ACTIVE)
8790
.map(this::mapToResponse)
8891
.orElse(null);
8992
}
9093

94+
@Transactional
95+
public boolean syncActivePurchasePrice(Integer variantId, BigDecimal purchasePrice) {
96+
if (purchasePrice == null) {
97+
return false;
98+
}
99+
100+
VariantPrice activePrice = variantPriceRepository
101+
.findFirstByVariantIdAndStatus(variantId, ACTIVE)
102+
.orElse(null);
103+
if (activePrice == null) {
104+
return false;
105+
}
106+
107+
activePrice.setPurchasePrice(purchasePrice);
108+
variantPriceRepository.save(activePrice);
109+
return true;
110+
}
111+
91112
/**
92113
* Cập nhật ngày hiệu lực của giá đang ACTIVE.
93114
*/

backend/src/main/java/com/smalltrend/service/inventory/PurchaseOrderService.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.smalltrend.entity.*;
66
import com.smalltrend.entity.enums.PurchaseOrderStatus;
77
import com.smalltrend.repository.*;
8+
import com.smalltrend.service.VariantPriceService;
89
import lombok.RequiredArgsConstructor;
910
import lombok.extern.slf4j.Slf4j;
1011
import org.springframework.stereotype.Service;
@@ -13,7 +14,9 @@
1314
import java.math.BigDecimal;
1415
import java.time.LocalDate;
1516
import java.util.ArrayList;
17+
import java.util.HashSet;
1618
import java.util.List;
19+
import java.util.Set;
1720
import java.util.stream.Collectors;
1821

1922
@Service
@@ -32,6 +35,7 @@ public class PurchaseOrderService {
3235
private final LocationRepository locationRepository;
3336
private final StockMovementRepository stockMovementRepository;
3437
private final UnitConversionRepository unitConversionRepository;
38+
private final VariantPriceService variantPriceService;
3539

3640
// ═══════════════════════════════════════════════════════════
3741
// Public API
@@ -284,8 +288,28 @@ public PurchaseOrderResponse receiveGoods(Integer orderId, GoodsReceiptRequest r
284288

285289
updateStock(order, itemRequests);
286290

291+
int syncedPurchasePriceCount = 0;
292+
Set<Integer> processedVariantIds = new HashSet<>();
293+
for (PurchaseOrderItem item : order.getItems()) {
294+
if (item.getVariant() == null || item.getVariant().getId() == null || item.getUnitCost() == null) {
295+
continue;
296+
}
297+
298+
Integer variantId = item.getVariant().getId();
299+
if (!processedVariantIds.add(variantId)) {
300+
continue;
301+
}
302+
303+
boolean synced = variantPriceService.syncActivePurchasePrice(variantId, item.getUnitCost());
304+
if (synced) {
305+
syncedPurchasePriceCount++;
306+
}
307+
}
308+
287309
log.info("📦 Purchase Order {} RECEIVED. Stock updated.", order.getOrderNumber());
288-
return toDetailResponse(order);
310+
PurchaseOrderResponse response = toDetailResponse(order);
311+
response.setSyncedPurchasePriceCount(syncedPurchasePriceCount);
312+
return response;
289313
}
290314

291315
// ─── Confirm Existing Draft ──────────────────────────────

backend/src/main/java/com/smalltrend/service/products/ProductVariantService.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,8 @@ public ProductVariantRespone updateVariant(Integer variantId, CreateVariantReque
188188
}
189189

190190
if (request.getCostPrice() != null) {
191-
List<ProductBatch> batches = productBatchRepository.findByVariantId(variantId);
192-
if (batches != null && !batches.isEmpty()) {
193-
ProductBatch latestBatch = batches.get(batches.size() - 1);
191+
ProductBatch latestBatch = productBatchRepository.findFirstByVariantIdOrderByIdDesc(variantId).orElse(null);
192+
if (latestBatch != null) {
194193
latestBatch.setCostPrice(request.getCostPrice());
195194
productBatchRepository.save(latestBatch);
196195
} else {
@@ -491,12 +490,8 @@ private ProductVariantRespone mapToResponse(ProductVariant variant) {
491490
response.setStockQuantity(stockQty);
492491

493492
// Get cost price from latest batch
494-
List<ProductBatch> batches = productBatchRepository.findByVariantId(variant.getId());
495-
if (batches != null && !batches.isEmpty()) {
496-
// Get the latest batch's cost price
497-
ProductBatch latestBatch = batches.get(batches.size() - 1);
498-
response.setCostPrice(latestBatch.getCostPrice());
499-
}
493+
productBatchRepository.findFirstByVariantIdOrderByIdDesc(variant.getId())
494+
.ifPresent(latestBatch -> response.setCostPrice(latestBatch.getCostPrice()));
500495

501496
// Get category and brand names
502497
if (variant.getProduct().getCategory() != null) {

frontend/src/components/layout/Sidebar.jsx

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
6767
{ label: "Danh mục & Thương hiệu", path: "/products/categories" },
6868
{ label: "Danh sách nhà cung cấp", path: "/products/suppliers" },
6969
{ label: "Danh sách sản phẩm", path: "/products" },
70-
{ label: "Thêm sản phẩm", path: "/products/addproduct" },
7170
{ label: "Thiết lập giá", path: "/products/price" },
7271
{ label: "Combo sản phẩm", path: "/products/combo" },
7372
],
@@ -158,13 +157,12 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
158157
{isAdmin && (
159158
<div className="mb-2">
160159
<div
161-
className={`flex items-center ${collapsed ? "justify-center" : "gap-3"} px-4 py-3 rounded-lg cursor-pointer transition-all duration-200 group ${
162-
location.pathname === "/dashboard" ||
160+
className={`flex items-center ${collapsed ? "justify-center" : "gap-3"} px-4 py-3 rounded-lg cursor-pointer transition-all duration-200 group ${location.pathname === "/dashboard" ||
163161
location.pathname.startsWith("/hr/users") ||
164162
openMenus["admin"]
165-
? "bg-indigo-50 text-indigo-700"
166-
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
167-
}`}
163+
? "bg-indigo-50 text-indigo-700"
164+
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
165+
}`}
168166
onClick={() =>
169167
collapsed ? navigate("/dashboard") : toggleMenu("admin")
170168
}
@@ -174,8 +172,8 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
174172
size={20}
175173
className={
176174
location.pathname === "/dashboard" ||
177-
location.pathname.startsWith("/hr/users") ||
178-
openMenus["admin"]
175+
location.pathname.startsWith("/hr/users") ||
176+
openMenus["admin"]
179177
? "text-indigo-600"
180178
: "text-slate-500 group-hover:text-slate-700"
181179
}
@@ -196,10 +194,9 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
196194
<NavLink
197195
to="/dashboard"
198196
className={({ isActive }) =>
199-
`block px-3 py-2 rounded-md text-sm transition-colors ${
200-
isActive
201-
? "bg-indigo-100 text-indigo-700 font-medium"
202-
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
197+
`block px-3 py-2 rounded-md text-sm transition-colors ${isActive
198+
? "bg-indigo-100 text-indigo-700 font-medium"
199+
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
203200
}`
204201
}
205202
>
@@ -208,10 +205,9 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
208205
<NavLink
209206
to="/hr/users"
210207
className={({ isActive }) =>
211-
`block px-3 py-2 rounded-md text-sm transition-colors ${
212-
isActive
213-
? "bg-indigo-100 text-indigo-700 font-medium"
214-
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
208+
`block px-3 py-2 rounded-md text-sm transition-colors ${isActive
209+
? "bg-indigo-100 text-indigo-700 font-medium"
210+
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
215211
}`
216212
}
217213
>
@@ -220,10 +216,9 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
220216
<NavLink
221217
to="/admin/ticket-center"
222218
className={({ isActive }) =>
223-
`block px-3 py-2 rounded-md text-sm transition-colors ${
224-
isActive
225-
? "bg-indigo-100 text-indigo-700 font-medium"
226-
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
219+
`block px-3 py-2 rounded-md text-sm transition-colors ${isActive
220+
? "bg-indigo-100 text-indigo-700 font-medium"
221+
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
227222
}`
228223
}
229224
>
@@ -232,10 +227,9 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
232227
<NavLink
233228
to="/admin/audit-logs"
234229
className={({ isActive }) =>
235-
`block px-3 py-2 rounded-md text-sm transition-colors ${
236-
isActive
237-
? "bg-indigo-100 text-indigo-700 font-medium"
238-
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
230+
`block px-3 py-2 rounded-md text-sm transition-colors ${isActive
231+
? "bg-indigo-100 text-indigo-700 font-medium"
232+
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
239233
}`
240234
}
241235
>
@@ -250,11 +244,10 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
250244
{navItems.map((item) => (
251245
<div key={item.label}>
252246
<div
253-
className={`flex items-center ${collapsed ? "justify-center" : "gap-3"} px-4 py-3 rounded-lg cursor-pointer transition-all duration-200 group ${
254-
location.pathname.startsWith(item.path) || openMenus[item.label]
255-
? "bg-indigo-50 text-indigo-700"
256-
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
257-
}`}
247+
className={`flex items-center ${collapsed ? "justify-center" : "gap-3"} px-4 py-3 rounded-lg cursor-pointer transition-all duration-200 group ${location.pathname.startsWith(item.path) || openMenus[item.label]
248+
? "bg-indigo-50 text-indigo-700"
249+
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
250+
}`}
258251
onClick={() =>
259252
collapsed ? navigate(item.path) : toggleMenu(item.label)
260253
}
@@ -264,7 +257,7 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
264257
size={20}
265258
className={
266259
location.pathname.startsWith(item.path) ||
267-
openMenus[item.label]
260+
openMenus[item.label]
268261
? "text-indigo-600"
269262
: "text-slate-500 group-hover:text-slate-700"
270263
}
@@ -291,10 +284,9 @@ const Sidebar = ({ collapsed, onToggleSidebar }) => {
291284
key={child.path}
292285
to={child.path}
293286
className={({ isActive }) =>
294-
`block px-3 py-2 rounded-md text-sm transition-colors ${
295-
isActive
296-
? "bg-indigo-100 text-indigo-700 font-medium"
297-
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
287+
`block px-3 py-2 rounded-md text-sm transition-colors ${isActive
288+
? "bg-indigo-100 text-indigo-700 font-medium"
289+
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
298290
}`
299291
}
300292
end={child.path === item.path}

frontend/src/hooks/usePurchaseOrder.js

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
createOrderItem,
2323
calcOrderFinancials,
2424
validateDraft,
25-
validateConfirm,
26-
canTransitionTo,
2725
} from "../utils/purchaseOrder";
2826

2927
export function usePurchaseOrder(initialId = null) {
@@ -507,9 +505,49 @@ export function usePurchaseOrder(initialId = null) {
507505
unitCost: toNumber(ri.unitCost),
508506
})),
509507
};
510-
await receiveGoodsOrder(initialId, receiptData);
508+
const response = await receiveGoodsOrder(initialId, receiptData);
509+
510+
const syncedCount = Number(response?.syncedPurchasePriceCount) || 0;
511+
const successMessage =
512+
syncedCount > 0
513+
? `Đã xác nhận nhập kho thành công! Đã đồng bộ giá nhập cho ${syncedCount} sản phẩm ở Thiết lập giá.`
514+
: "Đã xác nhận nhập kho thành công!";
515+
516+
if (syncedCount > 0) {
517+
const previousItemById = new Map(items.map((item) => [item.id, item]));
518+
const changedItems = receiptItems
519+
.map((ri) => {
520+
const prev = previousItemById.get(ri.itemId);
521+
const previousCost = toNumber(prev?.unit_price ?? prev?.unitCost);
522+
const newCost = toNumber(ri.unitCost);
523+
524+
if (!prev || newCost <= 0 || newCost === previousCost) {
525+
return null;
526+
}
527+
528+
return {
529+
itemId: ri.itemId,
530+
variantId: ri.variantId ?? prev.variantId ?? prev.variant_id ?? null,
531+
sku: prev.sku || "",
532+
productName: prev.name || "Sản phẩm",
533+
purchasePrice: newCost,
534+
previousPurchasePrice: previousCost,
535+
};
536+
})
537+
.filter(Boolean);
538+
539+
sessionStorage.setItem(
540+
"priceSyncNotice",
541+
JSON.stringify({
542+
syncedCount,
543+
orderNumber: response?.orderNumber || order?.po_number || null,
544+
syncedItems: changedItems,
545+
syncedAt: new Date().toISOString(),
546+
}),
547+
);
548+
}
511549

512-
toast.success("Đã xác nhận nhập kho và cập nhật tồn kho thành công!");
550+
toast.success(successMessage);
513551
if (navigate) navigate("/inventory/purchase-orders");
514552
return true;
515553
} catch (err) {
@@ -523,11 +561,13 @@ export function usePurchaseOrder(initialId = null) {
523561
[
524562
initialId,
525563
receiptItems,
564+
items,
526565
order.notes,
527566
order.supplier_id,
528567
order.location_id,
529568
order.tax_percent,
530569
order.shipping_fee,
570+
order.po_number,
531571
checkingFinancials,
532572
],
533573
);

0 commit comments

Comments
 (0)