Skip to content

Commit 200cc32

Browse files
committed
fix: final
1 parent 751b855 commit 200cc32

File tree

5 files changed

+48
-61
lines changed

5 files changed

+48
-61
lines changed

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)