Skip to content

Commit 160b615

Browse files
committed
Merge branch 'dev'
2 parents b515a5a + 200cc32 commit 160b615

File tree

6 files changed

+53
-66
lines changed

6 files changed

+53
-66
lines changed

backend/src/main/resources/data.sql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ INSERT INTO `role_permissions` VALUES (1,1,1),(2,2,1),(3,3,1),(4,4,1),(5,5,1),(6
9393
INSERT INTO `users` (`id`,`active`,`address`,`created_at`,`email`,`full_name`,`password`,`phone`,`status`,`updated_at`,`username`,`role_id`,`avatar_url`,`base_salary`,`count_late_as_present`,`hourly_rate`,`min_required_shifts`,`salary_type`,`working_hours_per_month`) VALUES (1,1,'123 Nguyen Hue, HCMC','2026-03-18 01:40:08.000000','admin@smalltrend.com','Nguyen Van Admin','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0901234567','ACTIVE','2026-03-18 01:40:08.000000','admin',1,'https://i.pravatar.cc/150?img=12',30000000.00,0,NULL,NULL,'MONTHLY',208.00),(2,1,'456 Le Loi, HCMC','2026-03-18 01:40:08.000000','manager@smalltrend.com','Tran Thi Manager','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0912345678','ACTIVE','2026-03-19 17:52:22.933689','manager',2,'https://res.cloudinary.com/didvvefmu/image/upload/v1773942746/smalltrend/avatars/pqsiai7remow3cpxfx3d.jpg',18000000.00,0,NULL,NULL,'MONTHLY',208.00),(3,1,'789 Dien Bien Phu, HCMC','2026-03-18 01:40:08.000000','cashier1@smalltrend.com','Le Van Cashier','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0923456789','ACTIVE','2026-03-18 01:40:08.000000','cashier1',3,'https://i.pravatar.cc/150?img=15',13500000.00,0,75000.00,NULL,'HOURLY',208.00),(4,1,'321 Ba Trieu, HCMC','2026-03-18 01:40:08.000000','cashier2@smalltrend.com','Vo Thi Cashier 2','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0968765432','ACTIVE','2026-03-18 01:40:08.000000','cashier2',3,'https://i.pravatar.cc/150?img=47',13200000.00,0,72000.00,NULL,'HOURLY',208.00),(5,1,'12 Nguyen Trai, HCMC','2026-03-18 01:40:08.000000','inventory@smalltrend.com','Pham Van Inventory','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0934567890','ACTIVE','2026-03-18 01:40:08.000000','inventory1',4,'https://i.pravatar.cc/150?img=25',13000000.00,0,NULL,NULL,'MONTHLY',208.00),(6,1,'90 Pasteur, HCMC','2026-03-18 01:40:08.000000','sales@smalltrend.com','Hoang Thi Sales','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0945678901','ACTIVE','2026-03-18 01:40:08.000000','sales1',5,'https://i.pravatar.cc/150?img=41',12600000.00,0,70000.00,NULL,'HOURLY',208.00),(7,1,'45 Hai Ba Trung, HCMC','2026-03-18 01:40:08.000000','sales2@smalltrend.com','Nguyen Van Sales 2','$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG','0987654012','ACTIVE','2026-03-18 01:40:08.000000','sales2',5,'https://i.pravatar.cc/150?img=6',12500000.00,0,NULL,20,'MONTHLY_MIN_SHIFTS',208.00),(8,1,'120 Yên Lãng, Đống Đa, Hà Nội','2026-03-19 14:24:14.936732','kiennguyen21005@gmail.com','Nguyễn Xuân Kiên','$2a$10$4eO2jrzTRQmOTW/iSlECv.99/YUjwzsVWIeIViQdjQw0YwEp7ZKNi','0842561752','ACTIVE','2026-03-19 14:24:14.936732','kien',2,NULL,NULL,0,NULL,NULL,'MONTHLY',208.00),(9,1,'Lào Cai','2026-03-19 14:25:32.081856','hung@gmail.com','Nguyễn Quốc Hưng','$2a$10$KT2Gw8KbGyljHUIo18ebeebchc8PjJyjfnJNRf2PnDXtV3rqFXjv2','0977869300','ACTIVE','2026-03-19 14:25:32.081856','hung',2,NULL,NULL,0,NULL,NULL,'MONTHLY',208.00);
9494

9595
-- 6. CUSTOMER TIERS
96-
INSERT INTO `customer_tiers` (`id`,`tier_code`,`tier_name`,`min_spending`,`min_points`,`points_multiplier`,`bonus_points`,`color`,`icon_url`,`free_shipping`,`priority_support`,`early_access`,`birthday_bonus`,`birthday_bonus_points`,`expiry_months`,`benefits`,`priority`,`is_active`,`description`,`created_at`,`updated_at`) VALUES
97-
(1,'BRONZE','Đồng',0.00,0,1.00,NULL,'#CD7F32',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,0,NULL,NULL,NULL),
98-
(2,'SILVER','Bạc',5000000.00,0,1.50,NULL,'#C0C0C0',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,2,0,NULL,NULL,'2026-03-19 15:33:09.341890'),
99-
(3,'GOLD','Vàng',15000000.00,0,2.00,NULL,'#FFD700',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,3,0,NULL,NULL,NULL),
100-
(4,'PLATINUM','Bạch Kim',50000000.00,0,3.00,NULL,'#E5E4E2',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,4,0,NULL,NULL,NULL);
96+
INSERT INTO `customer_tiers` (`id`,`tier_code`,`tier_name`,`min_spending`,`points_multiplier`,`bonus_points`,`color`,`icon_url`,`free_shipping`,`priority_support`,`early_access`,`birthday_bonus`,`birthday_bonus_points`,`expiry_months`,`benefits`,`priority`,`is_active`,`description`,`created_at`,`updated_at`) VALUES
97+
(1,'BRONZE','Đồng',0.00,1.00,NULL,'#CD7F32',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1,0,NULL,NULL,NULL),
98+
(2,'SILVER','Bạc',5000000.00,1.50,NULL,'#C0C0C0',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,2,0,NULL,NULL,'2026-03-19 15:33:09.341890'),
99+
(3,'GOLD','Vàng',15000000.00,2.00,NULL,'#FFD700',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,3,0,NULL,NULL,NULL),
100+
(4,'PLATINUM','Bạch Kim',50000000.00,3.00,NULL,'#E5E4E2',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,4,0,NULL,NULL,NULL);
101101

102102
-- 7. CUSTOMERS
103103
INSERT INTO `customers` (`id`,`loyalty_points`,`name`,`phone`,`spent_amount`) VALUES (1,150,'Nguyen Van A','0987654321',1200000),(2,800,'Tran Thi B','0976543210',6200000),(3,2000,'Le Van C','0965432109',18000000),(4,3382,'Pham Thi D','0954321098',56340200),(7,4,'Huy','0961390486',48000),(9,0,'s','09999999999',0),(10,0,'Ko','0961390487',0),(11,0,'Huy','0961390488',0),(12,0,'Huy','0123456789',0),(13,0,'','09612345688',0);

frontend/src/components/product/PriceTable.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Button from "./button";
2424
import { Input } from "./input";
2525
import { calculateProfit, formatCurrency } from "../../utils/priceCalculation";
2626

27-
const DateInput = ({ dateValue, onChange, disabled = false }) => {
27+
const DateInput = ({ dateValue, onChange, disabled = false, minDate }) => {
2828
const [focused, setFocused] = React.useState(false);
2929
const dateOnly = dateValue ? dateValue.split('T')[0] : '';
3030

@@ -41,6 +41,8 @@ const DateInput = ({ dateValue, onChange, disabled = false }) => {
4141
onFocus={() => setFocused(true)}
4242
onBlur={() => setFocused(false)}
4343
disabled={disabled}
44+
readOnly={!focused}
45+
min={focused ? minDate : undefined}
4446
className="w-[85px] text-center text-[11px] font-semibold text-gray-700 bg-gray-50 hover:bg-white border rounded-md px-1 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 cursor-pointer shadow-sm transition-all"
4547
/>
4648
);
@@ -72,6 +74,12 @@ export default function PriceTable({
7274
onExpiryDateChange,
7375
focusedVariantId = null
7476
}) {
77+
const today = React.useMemo(() => {
78+
const now = new Date();
79+
const tzOffset = now.getTimezoneOffset() * 60000;
80+
return new Date(now.getTime() - tzOffset).toISOString().split("T")[0];
81+
}, []);
82+
7583
const allSelected = variants.length > 0 && selectedIds.length === variants.length;
7684
const someSelected = selectedIds.length > 0 && !allSelected;
7785

@@ -282,6 +290,7 @@ export default function PriceTable({
282290
dateValue={variant.activeExpiryDate}
283291
onChange={(val) => onExpiryDateChange?.(variant, val)}
284292
disabled={readOnly}
293+
minDate={today}
285294
/>
286295
) : (
287296
<span className="text-[11px] text-gray-400 italic"></span>

frontend/src/pages/CRM/loyalty.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,6 @@ const GiftRewardManagement = () => {
591591
<div className="p-6 space-y-5">
592592
{/* Tìm SKU hoặc tên sản phẩm */}
593593
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
594-
<label className="block text-sm font-medium text-slate-700 mb-2">Tìm sản phẩm theo SKU hoặc tên</label>
595594
<label className="block text-sm font-medium text-slate-700 mb-2">Tìm sản phẩm theo SKU hoặc tên</label>
596595
<div className="flex gap-2 mb-3">
597596
<input

frontend/src/pages/Products/ProductManager/PriceSetting.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ const PRICE_SYNC_HISTORY_KEY = "priceSyncNotifications";
4040
const PRICE_SYNC_BROADCAST_KEY = "priceSyncNoticeBroadcast";
4141
const MAX_PRICE_SYNC_NOTIFICATIONS = 30;
4242

43+
const getTodayDateInput = () => {
44+
const now = new Date();
45+
const tzOffset = now.getTimezoneOffset() * 60000;
46+
return new Date(now.getTime() - tzOffset).toISOString().split("T")[0];
47+
};
48+
4349
const formatSyncedItemLabel = (item) => {
4450
const name = item?.productName || "Sản phẩm";
4551
const sku = item?.sku || "-";
@@ -442,6 +448,13 @@ const PriceSetting = () => {
442448
};
443449

444450
const handleExpiryDateChange = async (variant, newDateStr) => {
451+
const today = getTodayDateInput();
452+
if (newDateStr && newDateStr < today) {
453+
setErrorMsg("Ngày hết hiệu lực không được nhỏ hơn ngày hiện tại.");
454+
setTimeout(() => setErrorMsg(""), 4000);
455+
return;
456+
}
457+
445458
try {
446459
await api.put(`/products/variants/${variant.id}/prices/active/expiry`, { expiryDate: newDateStr || null });
447460
setSuccessMsg(`Đã cập nhật ngày hết hiệu lực cho ${variant.name}`);

frontend/src/pages/Products/ProductManager/Suppliers.jsx

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ const initialFormData = {
3333
phone: "",
3434
email: "",
3535
address: "",
36-
status: "active",
3736
};
3837

3938
const mapSupplierToFormData = (supplier) => ({
@@ -42,15 +41,13 @@ const mapSupplierToFormData = (supplier) => ({
4241
phone: supplier?.phone || "",
4342
email: supplier?.email || "",
4443
address: supplier?.address || "",
45-
status: supplier?.status || "active",
4644
});
4745

4846
export function SuppliersScreen() {
4947
// REVIEW FLOW: fetch danh sách nhà cung cấp -> lọc/tìm kiếm -> mở modal add/edit -> gọi API lưu/xoá -> refresh danh sách.
5048
const { user } = useAuth();
5149
const canEditProducts = canManageProducts(user);
5250
const [searchQuery, setSearchQuery] = useState("");
53-
const [filterStatus, setFilterStatus] = useState("all");
5451
const [isModalOpen, setIsModalOpen] = useState(false);
5552
const [editingSupplier, setEditingSupplier] = useState(null);
5653
const [formData, setFormData] = useState(initialFormData);
@@ -97,23 +94,17 @@ export function SuppliersScreen() {
9794
setTimeout(() => setToast(""), 3000);
9895
};
9996

100-
// REVIEW FLOW (LIST): chuẩn hoá nguồn suppliers -> filter theo keyword + trạng thái -> render bảng kết quả.
97+
// REVIEW FLOW (LIST): chuẩn hoá nguồn suppliers -> filter theo keyword -> render bảng kết quả.
10198
const filteredSuppliers = useMemo(() => {
10299
return (suppliers || []).filter((supplier) => {
103-
const matchesSearch =
100+
return (
104101
supplier.name?.toLowerCase()?.includes(searchQuery.toLowerCase()) ||
105102
supplier.contact_person?.toLowerCase()?.includes(searchQuery.toLowerCase()) ||
106103
supplier.phone?.includes(searchQuery) ||
107-
supplier.email?.toLowerCase()?.includes(searchQuery.toLowerCase());
108-
109-
const matchesStatus =
110-
filterStatus === "all" ||
111-
(filterStatus === "active" && supplier.status === "active") ||
112-
(filterStatus === "inactive" && supplier.status === "inactive");
113-
114-
return matchesSearch && matchesStatus;
104+
supplier.email?.toLowerCase()?.includes(searchQuery.toLowerCase())
105+
);
115106
});
116-
}, [suppliers, searchQuery, filterStatus]);
107+
}, [suppliers, searchQuery]);
117108

118109
const handleAdd = () => {
119110
if (!canEditProducts) return;
@@ -136,6 +127,7 @@ export function SuppliersScreen() {
136127
const payload = {
137128
...formData,
138129
contact_person: formData.contact_person?.trim(),
130+
status: editingSupplier?.status || "active",
139131
};
140132

141133
const result = editingSupplier
@@ -205,7 +197,7 @@ export function SuppliersScreen() {
205197
<h1 className="text-3xl font-bold text-gray-900">Quản lý nhà cung cấp</h1>
206198
<p className="text-gray-600 mt-2">
207199
Tổng số: <span className="font-semibold text-blue-600">{filteredSuppliers.length}</span> nhà cung cấp
208-
{searchQuery || filterStatus !== "all" ? <span className="text-gray-400"> (đang lọc)</span> : null}
200+
{searchQuery ? <span className="text-gray-400"> (đang lọc)</span> : null}
209201
</p>
210202
</div>
211203

@@ -245,11 +237,11 @@ export function SuppliersScreen() {
245237
<Card className="border-0 shadow-xl bg-white/90 backdrop-blur-sm rounded-2xl overflow-hidden">
246238
<div className="bg-gradient-to-r from-blue-600 via-indigo-600 to-violet-600 px-6 py-3">
247239
<h3 className="text-white font-bold text-sm">Tìm kiếm & bộ lọc</h3>
248-
<p className="text-blue-100 text-xs">Lọc danh sách nhà cung cấp theo từ khóa và trạng thái</p>
240+
<p className="text-blue-100 text-xs">Lọc danh sách nhà cung cấp theo từ khóa</p>
249241
</div>
250242
<CardContent className="p-5">
251-
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
252-
<div className="relative md:col-span-2">
243+
<div className="grid grid-cols-1 gap-4">
244+
<div className="relative">
253245
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
254246
<Input
255247
placeholder="Tìm theo tên, người liên hệ, SĐT hoặc email..."
@@ -258,16 +250,6 @@ export function SuppliersScreen() {
258250
onChange={(e) => setSearchQuery(e.target.value)}
259251
/>
260252
</div>
261-
262-
<select
263-
className="w-full h-11 px-4 text-base bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent appearance-none"
264-
value={filterStatus}
265-
onChange={(e) => setFilterStatus(e.target.value)}
266-
>
267-
<option value="all">Tất cả trạng thái</option>
268-
<option value="active">Đang hợp tác</option>
269-
<option value="inactive">Ngưng hợp tác</option>
270-
</select>
271253
</div>
272254
</CardContent>
273255
</Card>
@@ -292,7 +274,6 @@ export function SuppliersScreen() {
292274
<TableHead className="font-bold text-gray-700">Nhà cung cấp</TableHead>
293275
<TableHead className="font-bold text-gray-700">Liên hệ</TableHead>
294276
<TableHead className="font-bold text-gray-700">Địa chỉ</TableHead>
295-
<TableHead className="font-bold text-gray-700">Trạng thái</TableHead>
296277
<TableHead className="text-center font-bold text-gray-700">Thao tác</TableHead>
297278
</TableRow>
298279
</TableHeader>
@@ -339,14 +320,6 @@ export function SuppliersScreen() {
339320
</div>
340321
</TableCell>
341322

342-
<TableCell>
343-
{supplier.status === "active" ? (
344-
<Badge className="bg-green-50 text-green-700 border border-green-200">Đang hợp tác</Badge>
345-
) : (
346-
<Badge className="bg-red-50 text-red-700 border border-red-200">Ngưng hợp tác</Badge>
347-
)}
348-
</TableCell>
349-
350323
<TableCell className="text-center">
351324
{canEditProducts && (
352325
<div className="flex justify-center gap-2">
@@ -547,21 +520,6 @@ export function SuppliersScreen() {
547520
required
548521
/>
549522
</div>
550-
<div>
551-
<Label>
552-
Trạng thái <span className="text-red-600">*</span>
553-
</Label>
554-
<select
555-
name="status"
556-
className="w-full mt-2 h-11 px-4 border border-gray-200 rounded-xl bg-gray-50"
557-
value={formData.status}
558-
onChange={handleChange}
559-
required
560-
>
561-
<option value="active">Đang hợp tác</option>
562-
<option value="inactive">Ngưng hợp tác</option>
563-
</select>
564-
</div>
565523
</div>
566524
</CardContent>
567525
</Card>

frontend/src/pages/Products/ProductManager/UnitConversionSection.jsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,32 @@ export default function UnitConversionSection({ variant, units, onSuccess }) {
6060
setIsDeleting(true);
6161
setDeleteStatusMsg("");
6262

63-
deleteTimeoutRef.current = setTimeout(() => {
64-
setDeleteStatusMsg("Không thể xoá quy đổi vì thao tác đã quá 2 phút. Vui lòng thử lại.");
65-
}, 120000);
66-
6763
try {
68-
await api.delete(`/products/conversions/${id}`);
64+
const timeoutPromise = new Promise((_, reject) => {
65+
deleteTimeoutRef.current = setTimeout(() => {
66+
reject(new Error("Không thể xoá quy đổi vì thao tác đã quá 2 phút. Vui lòng thử lại."));
67+
}, 120000);
68+
});
69+
70+
await Promise.race([
71+
api.delete(`/products/conversions/${id}`),
72+
timeoutPromise,
73+
]);
74+
6975
setConversions(prev => prev.filter(c => c.id !== id));
7076
if (onSuccess) onSuccess();
77+
setDeleteStatusMsg("");
78+
setConfirmDeleteId(null);
7179
} catch (err) {
7280
console.error("Lỗi xóa quy đổi:", err);
81+
const msg = err?.response?.data?.message || err?.response?.data || err?.message || "Không thể xoá quy đổi. Vui lòng thử lại.";
82+
setDeleteStatusMsg(typeof msg === "string" ? msg : "Không thể xoá quy đổi. Vui lòng thử lại.");
7383
} finally {
7484
if (deleteTimeoutRef.current) {
7585
clearTimeout(deleteTimeoutRef.current);
7686
deleteTimeoutRef.current = null;
7787
}
7888
setIsDeleting(false);
79-
setDeleteStatusMsg("");
80-
setConfirmDeleteId(null);
8189
}
8290
};
8391

0 commit comments

Comments
 (0)