Skip to content

Commit c571bee

Browse files
authored
Merge pull request #15 from touchmegit1/dev
Dev
2 parents 6f8ecdd + 35835ac commit c571bee

File tree

9 files changed

+227
-161
lines changed

9 files changed

+227
-161
lines changed

.github/workflows/cd.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,27 @@ jobs:
172172
script: |
173173
set -eu
174174
cd "$DEPLOY_PATH"
175+
ENV_FILE="./deploy/env/backend.env"
175176
176-
if [ ! -f "./deploy/env/backend.env" ]; then
177-
echo "Missing required file: ./deploy/env/backend.env"
177+
if [ ! -f "$ENV_FILE" ]; then
178+
echo "Missing required file: $ENV_FILE"
178179
exit 1
179180
fi
180181
182+
# Load backend env even when file is owned by root (e.g. mode 600).
181183
set -a
182-
. ./deploy/env/backend.env
184+
TMP_ENV="$(mktemp)"
185+
if [ -r "$ENV_FILE" ]; then
186+
cat "$ENV_FILE" > "$TMP_ENV"
187+
else
188+
echo "No read permission for $ENV_FILE, loading via sudo..."
189+
sudo cat "$ENV_FILE" > "$TMP_ENV"
190+
fi
191+
192+
# Normalize potential CRLF and source variables
193+
sed -i 's/\r$//' "$TMP_ENV"
194+
. "$TMP_ENV"
195+
rm -f "$TMP_ENV"
183196
set +a
184197
185198
: "${DB_USERNAME:=smalltrend}"

backend/src/main/java/com/smalltrend/service/inventory/dashboard/InventoryDashboardService.java

Lines changed: 140 additions & 116 deletions
Large diffs are not rendered by default.

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

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@
22

33
import com.smalltrend.dto.products.UnitConversionRequest;
44
import com.smalltrend.dto.products.UnitConversionResponse;
5+
import com.smalltrend.entity.InventoryStock;
56
import com.smalltrend.entity.Product;
6-
import com.smalltrend.entity.ProductVariant;
77
import com.smalltrend.entity.ProductBatch;
8-
import com.smalltrend.entity.InventoryStock;
8+
import com.smalltrend.entity.ProductVariant;
99
import com.smalltrend.entity.Unit;
1010
import com.smalltrend.entity.UnitConversion;
1111
import com.smalltrend.repository.InventoryStockRepository;
1212
import com.smalltrend.repository.ProductBatchRepository;
1313
import com.smalltrend.repository.ProductVariantRepository;
1414
import com.smalltrend.repository.UnitConversionRepository;
1515
import com.smalltrend.repository.UnitRepository;
16-
1716
import lombok.RequiredArgsConstructor;
1817
import org.springframework.stereotype.Service;
1918
import org.springframework.transaction.annotation.Transactional;
2019

21-
import java.time.LocalDate;
20+
import java.math.BigDecimal;
2221
import java.math.RoundingMode;
22+
import java.time.LocalDate;
2323
import java.util.Collections;
2424
import java.util.List;
2525
import java.util.Map;
@@ -58,7 +58,7 @@ public List<UnitConversionResponse> getConversionsByVariantId(Integer variantId)
5858
public UnitConversionResponse addConversion(Integer variantId, UnitConversionRequest request) {
5959
ProductVariant baseVariant = productVariantRepository.findById(variantId)
6060
.orElseThrow(() -> new RuntimeException(
61-
"Không tìm thấy biến thể với ID: " + variantId));
61+
"Không tìm thấy biến thể với ID: " + variantId));
6262

6363
Integer productId = baseVariant.getProduct() != null ? baseVariant.getProduct().getId() : null;
6464
Integer unitId = baseVariant.getUnit() != null ? baseVariant.getUnit().getId() : null;
@@ -67,20 +67,19 @@ public UnitConversionResponse addConversion(Integer variantId, UnitConversionReq
6767
List<UnitConversion> conversionsToThisUnit = unitConversionRepository.findByProductIdAndToUnitId(productId, unitId);
6868
boolean isConversionDerivedVariant = conversionsToThisUnit.stream()
6969
.filter(conversion -> conversion != null
70-
&& conversion.getVariant() != null
71-
&& conversion.getVariant().getId() != null
72-
&& !conversion.getVariant().getId().equals(baseVariant.getId()))
70+
&& conversion.getVariant() != null
71+
&& conversion.getVariant().getId() != null
72+
&& !conversion.getVariant().getId().equals(baseVariant.getId()))
7373
.anyMatch(conversion -> hasSameAttributes(conversion.getVariant(), baseVariant));
7474

7575
if (isConversionDerivedVariant) {
7676
throw new RuntimeException("Chỉ biến thể đơn vị gốc mới được phép thêm quy đổi đơn vị.");
7777
}
7878
}
7979

80-
8180
Unit toUnit = unitRepository.findById(request.getToUnitId())
8281
.orElseThrow(() -> new RuntimeException(
83-
"Không tìm thấy đơn vị với ID: " + request.getToUnitId()));
82+
"Không tìm thấy đơn vị với ID: " + request.getToUnitId()));
8483

8584
if (unitConversionRepository.existsByVariantIdAndToUnitId(variantId, request.getToUnitId())) {
8685
throw new RuntimeException(
@@ -89,38 +88,41 @@ public UnitConversionResponse addConversion(Integer variantId, UnitConversionReq
8988

9089
Product product = baseVariant.getProduct();
9190

91+
BigDecimal resolvedSellPrice = request.getSellPrice() != null
92+
? request.getSellPrice()
93+
: baseVariant.getSellPrice();
94+
if (resolvedSellPrice == null) {
95+
throw new RuntimeException("Giá bán quy đổi không được để trống.");
96+
}
97+
9298
// ─── 1. Tạo quy đổi đơn vị ────────────────────────────────────────────
9399
UnitConversion conversion = UnitConversion.builder()
94100
.variant(baseVariant)
95101
.toUnit(toUnit)
96102
.conversionFactor(request.getConversionFactor())
97-
.sellPrice(request.getSellPrice())
103+
.sellPrice(resolvedSellPrice)
98104
.description(request.getDescription())
99105
.isActive(request.getIsActive() != null ? request.getIsActive() : true)
100106
.build();
101107

102108
UnitConversion savedConversion = unitConversionRepository.save(conversion);
103109

104110
// ─── 2. Tự động tạo Product Variant mới cho đơn vị đóng gói ───────────
105-
// Sinh SKU: VD BEV-COCA-COLA-LOC6
106111
String autoSku = productVariantService.generateSkuForConversion(
107112
baseVariant, toUnit, request.getConversionFactor());
108113

109-
// Tạo variant mới với đơn vị đích.
110-
// Copy attributes từ base variant (mỗi variant có variant_id riêng nên không xung đột key).
111114
ProductVariant packagingVariant = ProductVariant.builder()
112115
.product(product)
113116
.sku(autoSku)
114117
.unit(toUnit)
115-
.sellPrice(request.getSellPrice())
118+
.sellPrice(resolvedSellPrice)
116119
.isActive(baseVariant.isActive())
117120
.attributes(baseVariant.getAttributes() != null
118121
? new java.util.HashMap<>(baseVariant.getAttributes())
119122
: new java.util.HashMap<>())
120123
.build();
121124

122-
ProductVariant savedVariant = productVariantRepository.saveAndFlush(packagingVariant); // cần id ngay để sinh barcode
123-
125+
ProductVariant savedVariant = productVariantRepository.saveAndFlush(packagingVariant);
124126

125127
// ─── 3. Sinh barcode nội bộ ──────
126128
String autoBarcode = productVariantService.generateInternalBarcodeForPackaging(
@@ -174,22 +176,28 @@ public UnitConversionResponse addConversion(Integer variantId, UnitConversionReq
174176
public UnitConversionResponse updateConversion(Integer conversionId, UnitConversionRequest request) {
175177
UnitConversion conversion = unitConversionRepository.findById(conversionId)
176178
.orElseThrow(() -> new RuntimeException(
177-
"Không tìm thấy quy đổi với ID: " + conversionId));
179+
"Không tìm thấy quy đổi với ID: " + conversionId));
178180

179181
Unit toUnit = unitRepository.findById(request.getToUnitId())
180182
.orElseThrow(() -> new RuntimeException(
181-
"Không tìm thấy đơn vị với ID: " + request.getToUnitId()));
183+
"Không tìm thấy đơn vị với ID: " + request.getToUnitId()));
182184

183-
// Check duplicate (excluding current record)
184185
if (unitConversionRepository.existsByVariantIdAndToUnitIdAndIdNot(
185186
conversion.getVariant().getId(), request.getToUnitId(), conversionId)) {
186187
throw new RuntimeException(
187188
"Quy đổi sang đơn vị '" + toUnit.getName() + "' đã tồn tại cho biến thể này!");
188189
}
189190

191+
BigDecimal resolvedSellPrice = request.getSellPrice() != null
192+
? request.getSellPrice()
193+
: conversion.getSellPrice();
194+
if (resolvedSellPrice == null) {
195+
throw new RuntimeException("Giá bán quy đổi không được để trống.");
196+
}
197+
190198
conversion.setToUnit(toUnit);
191199
conversion.setConversionFactor(request.getConversionFactor());
192-
conversion.setSellPrice(request.getSellPrice());
200+
conversion.setSellPrice(resolvedSellPrice);
193201
conversion.setDescription(request.getDescription());
194202
if (request.getIsActive() != null) {
195203
conversion.setActive(request.getIsActive());
@@ -209,7 +217,6 @@ public void deleteConversion(Integer conversionId) {
209217

210218
unitConversionRepository.deleteById(conversionId);
211219

212-
// Xoá biến thể đóng gói được tạo tự động khi tạo quy đổi
213220
List<ProductVariant> autoVariants = productVariantRepository.findByProductIdAndUnitId(productId, toUnitId);
214221
if (autoVariants != null && !autoVariants.isEmpty()) {
215222
for (ProductVariant v : autoVariants) {

frontend/src/components/inventory/dashboard/StatsCards.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ function ProductsModal({ products, onClose }) {
102102

103103
return (
104104
<Modal
105-
title="Danh sách sản phẩm"
106-
subtitle={`${products.length} sản phẩm đang quản lý trong kho`}
105+
title="Danh sách biến thể"
106+
subtitle={`${products.length} biến thể đang quản lý trong kho`}
107107
icon={Package}
108108
iconBg="bg-blue-50"
109109
iconColor="text-blue-600"
@@ -119,7 +119,7 @@ function ProductsModal({ products, onClose }) {
119119
<div className="space-y-2">
120120
{filtered.length === 0 && (
121121
<p className="text-center text-sm text-slate-400 py-8">
122-
Không tìm thấy sản phẩm nào
122+
Không tìm thấy biến thể nào
123123
</p>
124124
)}
125125
{filtered.map((p) => {
@@ -163,7 +163,7 @@ function InventoryValueModal({ products, totalValue, onClose }) {
163163

164164
return (
165165
<Modal
166-
title="Giá trị tồn kho theo sản phẩm"
166+
title="Giá trị tồn kho theo biến thể"
167167
subtitle={`Tổng: ${formatCurrency(totalValue)} · Sắp xếp theo giá trị cao nhất`}
168168
icon={DollarSign}
169169
iconBg="bg-emerald-50"
@@ -486,8 +486,8 @@ export default function StatsCards({
486486
{
487487
id: "products",
488488
icon: Package,
489-
label: "Tổng sản phẩm",
490-
value: `${formatNumber(stats.productCount)} loại`,
489+
label: "Tổng biến thể",
490+
value: `${formatNumber(stats.productCount)} biến thể`,
491491
subtitle: `${formatNumber(stats.totalStockUnits)} đơn vị tồn kho`,
492492
iconBg: "bg-blue-50",
493493
iconColor: "text-blue-600",

frontend/src/components/inventory/dashboard/StockByProductChart.jsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function StockPanel({
131131
{/* Footer */}
132132
<div className="mt-4 pt-3 border-t border-slate-100 flex items-center justify-between">
133133
<p className="text-xs text-slate-400">Đơn vị: số lượng tồn kho</p>
134-
<p className="text-xs text-slate-400">{data.length} sản phẩm</p>
134+
<p className="text-xs text-slate-400">{data.length} biến thể</p>
135135
</div>
136136
</div>
137137
);
@@ -167,11 +167,11 @@ function StockByProductChart({ products }) {
167167
<div className="flex items-center gap-2 mb-4">
168168
<BarChart3 size={20} className="text-indigo-600" />
169169
<h2 className="text-lg font-semibold text-slate-900">
170-
Tồn kho theo sản phẩm
170+
Tồn kho theo biến thể
171171
</h2>
172172
</div>
173173
<p className="text-sm text-slate-500 text-center py-8">
174-
Không có dữ liệu sản phẩm
174+
Không có dữ liệu biến thể
175175
</p>
176176
</div>
177177
);
@@ -182,25 +182,25 @@ function StockByProductChart({ products }) {
182182
{/* Biểu đồ 1: Tồn kho cao */}
183183
<StockPanel
184184
title="Tồn kho cao"
185-
subtitle={`Top ${highStock.length} sản phẩm tồn kho dồi dào nhất`}
185+
subtitle={`Top ${highStock.length} biến thể tồn kho dồi dào nhất`}
186186
icon={TrendingUp}
187187
iconGradient="linear-gradient(135deg, #6366f1, #8b5cf6)"
188188
data={highStock}
189189
colors={HIGH_COLORS}
190190
maxStock={maxHigh}
191-
emptyMsg="Không có sản phẩm tồn kho cao"
191+
emptyMsg="Không có biến thể tồn kho cao"
192192
/>
193193

194194
{/* Biểu đồ 2: Tồn kho thấp */}
195195
<StockPanel
196196
title="Tồn kho thấp"
197-
subtitle={`Top ${lowStock.length} sản phẩm cần nhập hàng sớm`}
197+
subtitle={`Top ${lowStock.length} biến thể cần nhập hàng sớm`}
198198
icon={TrendingDown}
199199
iconGradient="linear-gradient(135deg, #ef4444, #f97316)"
200200
data={lowStock}
201201
colors={LOW_COLORS}
202202
maxStock={maxLow}
203-
emptyMsg="Không có sản phẩm cần cảnh báo tồn kho"
203+
emptyMsg="Không có biến thể cần cảnh báo tồn kho"
204204
/>
205205
</div>
206206
);

frontend/src/hooks/inventory/dashboard/useInventoryData.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ export function useInventoryDashboard() {
3434
const [categoryFilter, setCategoryFilter] = useState("all");
3535
const [stockFilter, setStockFilter] = useState("all");
3636
const [sortConfig, setSortConfig] = useState({ key: "name", direction: "asc" });
37+
38+
const buildVariantLabel = useCallback((item) => {
39+
const attrs = item.attributes && typeof item.attributes === "object"
40+
? Object.entries(item.attributes)
41+
.map(([, value]) => String(value || "").trim())
42+
.filter(Boolean)
43+
: [];
44+
45+
if (attrs.length === 0) {
46+
return item.name;
47+
}
48+
49+
const suffix = attrs.join(" · ");
50+
return item.name?.includes(" - ") ? item.name : `${item.name} - ${suffix}`;
51+
}, []);
3752
const [batchTab, setBatchTab] = useState("all"); // all | expired | expiring | safe
3853

3954
// ─── Fetch ─────────────────────────────────────────────────
@@ -83,14 +98,15 @@ export function useInventoryDashboard() {
8398
const inventoryValue = (p.stock_quantity || 0) * (p.purchase_price || 0);
8499
return {
85100
...p,
101+
name: buildVariantLabel(p),
86102
stockStatus,
87103
categoryName: category?.name || "—",
88104
brandName: brand?.name || "—",
89105
productBatches,
90106
inventoryValue,
91107
};
92108
});
93-
}, [products, categories, brands, batches]);
109+
}, [products, categories, brands, batches, buildVariantLabel]);
94110

95111
// ─── Enriched Batches ──────────────────────────────────────
96112
const enrichedBatches = useMemo(() => {

frontend/src/pages/Inventory/dashboard/InventoryDashboard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function InventoryDashboard() {
7575
Tổng quan kho hàng
7676
</h1>
7777
<p className="text-sm text-slate-500 mt-0.5">
78-
Quản lý tồn kho, lô hàng & cảnh báo hạn sử dụng
78+
Quản lý tồn kho theo biến thể, lô hàng & cảnh báo hạn sử dụng
7979
</p>
8080
</div>
8181
<div className="flex items-center gap-2">

frontend/src/pages/Pos/PaymentModal.jsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export default function PaymentModal({ cart, customer, onClose, onComplete, onSt
241241
const [loadingPromotions, setLoadingPromotions] = useState(false);
242242
const [paymentMethod, setPaymentMethod] = useState("cash");
243243
const [cashAmount, setCashAmount] = useState("");
244+
const [suggestionBaseAmount, setSuggestionBaseAmount] = useState("");
244245
const [focusedField, setFocusedField] = useState("customerSearch");
245246
const [suggestedIndex, setSuggestedIndex] = useState(-1);
246247
const [tiers, setTiers] = useState([]); // Danh sách hạng thành viên
@@ -562,13 +563,13 @@ export default function PaymentModal({ cart, customer, onClose, onComplete, onSt
562563

563564
// Lấy suggested amounts.
564565
const getSuggestedAmounts = () => {
565-
if (!cashAmount) return [];
566+
if (!suggestionBaseAmount) return [];
566567

567-
const cleanCashAmount = cashAmount.replace(/[^0-9]/g, '');
568-
const num = parseInt(cleanCashAmount, 10);
568+
const cleanBaseAmount = suggestionBaseAmount.replace(/[^0-9]/g, '');
569+
const num = parseInt(cleanBaseAmount, 10);
569570
if (isNaN(num) || num <= 0) return [];
570571

571-
const digitCount = cleanCashAmount.length;
572+
const digitCount = cleanBaseAmount.length;
572573
const startPower = Math.max(0, 4 - digitCount);
573574

574575
return [
@@ -1251,7 +1252,11 @@ export default function PaymentModal({ cart, customer, onClose, onComplete, onSt
12511252
type="number"
12521253
placeholder="Nhập số tiền"
12531254
value={cashAmount}
1254-
onChange={(e) => setCashAmount(e.target.value)}
1255+
onChange={(e) => {
1256+
const value = e.target.value;
1257+
setCashAmount(value);
1258+
setSuggestionBaseAmount(value);
1259+
}}
12551260
onFocus={() => setFocusedField("cashAmount")}
12561261
style={{
12571262
width: "100%",

frontend/src/services/inventory/inventoryService.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export const getDashboardProducts = async () => {
326326
brand_id: p.brandId,
327327
brand_name: p.brandName,
328328
attributes: p.attributes || null,
329+
unit: p.unit || "",
329330
}));
330331
};
331332

0 commit comments

Comments
 (0)