update template mail

This commit is contained in:
Admin 2025-05-23 15:52:58 +07:00
parent 43dcfc78bb
commit aea4169a50
24 changed files with 689 additions and 393 deletions

View File

@ -17,11 +17,13 @@ export default function DeleteRowAction({
setConfirm({
handleOk: async () => {
const result = await deletesBid(chooses);
console.log({ result });
if (!result) return;
onDeleted?.();
},
title: 'Delete',
message: `This action will remove ${chooses.length} products.`
title: "Delete",
message: `This action will remove ${chooses.length} products.`,
});
};

View File

@ -1,4 +1,2 @@
export { default as ShowHistoriesModal } from './show-histories-modal';
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
export { default as ShowHistoriesBidPicklesApiModal } from './show-histories-bid-pickles-api-modal';
export { default as BidModal } from './bid-modal';
export { default as ShowHistoriesModal } from "./show-histories-modal";
export { default as BidModal } from "./bid-modal";

View File

@ -0,0 +1,71 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import Table from "../../../lib/table/table";
import { IColumn } from "../../../lib/table/type";
import { formatTime } from "../../../utils";
export interface IGraysHistoriesViewProps {
histories: Record<string, string>[];
}
export default function AllbidsHistoriesView({
histories,
}: IGraysHistoriesViewProps) {
type BidHistoryEntry = {
row_id: number;
date: string; // ISO datetime string
amount: number; // Số tiền đặt giá
proxyamount: number; // Giá proxy tối đa
bidQty: number; // Số lượng
flashBuy: boolean; // Mua ngay
proxyBid: boolean; // Có phải đấu giá tự động
instantBid: boolean; // Đặt giá ngay
userName: string; // Tên người dùng
bidBidderID: number; // ID người đặt giá
$$hashKey?: string; // Khóa nội bộ Angular (không cần thiết, có thể bỏ hoặc để optional)
};
const columns: IColumn<BidHistoryEntry>[] = [
{
title: "Username",
key: "userName",
},
{
title: "Amount",
key: "amount",
},
{
title: "Proxy amount",
key: "proxyamount",
},
{
title: "Bid Qty",
key: "bidQty",
},
{
title: "Bid at",
key: "date",
renderRow(row) {
return <span>{formatTime(row.date)}</span>;
},
},
];
return (
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
styleDefaultHead={{
justifyContent: "flex-start",
width: "fit-content",
}}
showFilter={false}
showActions={false}
showChooses={false}
columns={columns}
rowKey="row_id"
rows={histories as unknown as BidHistoryEntry[]}
/>
);
}

View File

@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useMemo } from "react";
import { extractNumber, formatTime } from "../../../utils";
import Table from "../../../lib/table/table";
import { IColumn } from "../../../lib/table/type";
export interface IGraysHistoriesViewProps {
histories: Record<string, string>[];
}
export default function GraysHistoriesView({
histories,
}: IGraysHistoriesViewProps) {
type BidHistoryEntry = {
row_id: number;
Price: string;
Quantity: number;
WinningQuantity: number;
UserShortAddress: string;
UserInitials: string;
OriginalDate: string;
};
const columns: IColumn<BidHistoryEntry>[] = [
{
title: "Bidding Details",
key: "UserInitials",
renderRow(row) {
return (
<span>{`${row["UserInitials"]} - ${row["UserShortAddress"]}`}</span>
);
},
},
{
title: "Bid Time",
key: "OriginalDate",
renderRow(row) {
return (
<span>
{formatTime(
new Date(extractNumber(row["OriginalDate"]) || 0).toUTCString(),
"HH:mm:ss DD/MM/YYYY"
)}
</span>
);
},
},
{
title: "Bid Price",
key: "Price",
},
{
title: "Bid Qty",
key: "Quantity",
},
{
title: "Win Qty",
key: "WinningQuantity",
},
];
return (
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
styleDefaultHead={{
justifyContent: "flex-start",
width: "fit-content",
}}
showFilter={false}
showActions={false}
showChooses={false}
columns={columns}
rowKey="row_id"
rows={histories as unknown as BidHistoryEntry[]}
/>
);
}

View File

@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useMemo } from "react";
import { formatTime } from "../../../utils";
import { IColumn } from "../../../lib/table/type";
import Table from "../../../lib/table/table";
export interface IGraysHistoriesViewProps {
histories: Record<string, string>[];
}
export default function PicklesHistoriesView({
histories,
}: IGraysHistoriesViewProps) {
// const rows = useMemo(() => {
// return histories.map((element, index) => (
// <Table.Tr key={index}>
// <Table.Td>{element["bidderAnonName"]}</Table.Td>
// <Table.Td>{element["actualBid"]}</Table.Td>
// <Table.Td>
// {formatTime(
// new Date(element["bidTimeInMilliSeconds"]).toUTCString(),
// "HH:mm:ss DD/MM/YYYY"
// )}
// </Table.Td>
// </Table.Tr>
// ));
// }, [histories]);
type BidHistoryEntry = {
row_id: number;
bidderAnonName: string;
actualBid: number;
bidTimeInMilliSeconds: number;
};
const columns: IColumn<BidHistoryEntry>[] = [
{
title: "Bidder name",
key: "bidderAnonName",
},
{
title: "Actual bid",
key: "actualBid",
},
{
title: "Time",
key: "bidTimeInMilliSeconds",
renderRow(row) {
return (
<span>
{formatTime(
new Date(row["bidTimeInMilliSeconds"]).toUTCString(),
"HH:mm:ss DD/MM/YYYY"
)}
</span>
);
},
},
];
return (
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
styleDefaultHead={{
justifyContent: "flex-start",
width: "fit-content",
}}
showFilter={false}
showActions={false}
showChooses={false}
columns={columns}
rowKey="row_id"
rows={histories as unknown as BidHistoryEntry[]}
/>
);
}

View File

@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LoadingOverlay, Modal, ModalProps } from "@mantine/core";
import { useCallback, useEffect, useMemo, useState } from "react";
import { getDetailBidHistories } from "../../../apis/bid-histories";
import { IBid } from "../../../system/type";
import GraysHistoriesView from "./grays-histories-view";
import constants from "../../../constant";
import PicklesHistoriesView from "./pickles-histories-view";
import AllbidsHistoriesView from "./allbids-histories-view";
export interface IShowHistoriesApiModalProps extends ModalProps {
data: IBid | null;
onUpdated?: () => void;
}
export default function ShowHistoriesApiModal({
data,
onUpdated,
...props
}: IShowHistoriesApiModalProps) {
const [histories, setHistories] = useState<Record<string, string>[]>([]);
const [loading, setLoading] = useState(false);
const handleCallApi = useCallback(async () => {
if (!data?.lot_id) {
setHistories([]);
return;
}
setLoading(true);
const response = await getDetailBidHistories(data?.lot_id);
setLoading(false);
if (response.data && response.data) {
const values = (response.data as Record<string, string>[]).map(
(item, index) => {
return {
...item,
row_id: String(index),
};
}
);
setHistories(values);
}
}, [data]);
useEffect(() => {
handleCallApi();
}, [handleCallApi]);
const generateView = useMemo(() => {
switch (data?.web_bid.origin_url) {
case constants.grays:
return <GraysHistoriesView histories={histories} />;
case constants.pickles:
return <PicklesHistoriesView histories={histories} />;
case constants.allbids:
return <AllbidsHistoriesView histories={histories} />;
}
}, [data?.web_bid.origin_url, histories]);
return (
<Modal
className="relative"
{...props}
size="xl"
title={<span className="text-xl font-bold">BIDDING HISTORY</span>}
centered
>
{generateView}
<LoadingOverlay
visible={loading}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>
</Modal>
);
}

View File

@ -1,78 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { getDetailBidHistories } from '../../apis/bid-histories';
import { IBid } from '../../system/type';
import { extractNumber, formatTime } from '../../utils';
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
data: IBid | null;
onUpdated?: () => void;
}
export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
const [histories, setHistories] = useState<Record<string, string>[]>([]);
const [loading, setLoading] = useState(false);
const rows = useMemo(() => {
return histories.map((element, index) => (
<Table.Tr key={index}>
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
<Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
<Table.Td>{`AU $${element['Price']}`}</Table.Td>
<Table.Td>{`${element['Quantity']}`}</Table.Td>
<Table.Td>{`${element['WinningQuantity']}`}</Table.Td>
</Table.Tr>
));
}, [histories]);
const handleCallApi = useCallback(async () => {
if (!data?.lot_id) {
setHistories([]);
return;
}
setLoading(true);
const response = await getDetailBidHistories(data?.lot_id);
setLoading(false);
if (response.data && response.data) {
setHistories(response.data);
}
}, [data]);
useEffect(() => {
handleCallApi();
}, [handleCallApi]);
return (
<Modal className="relative" {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>Bidding Details</Table.Th>
<Table.Th>Bid Time</Table.Th>
<Table.Th>Bid Price</Table.Th>
<Table.Th>Bid Qty</Table.Th>
<Table.Th>Win Qty</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{histories.length <= 0 ? (
<Table.Tr>
<Table.Td colSpan={5} className="text-center">
None
</Table.Td>
</Table.Tr>
) : (
rows
)}
</Table.Tbody>
</Table>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,8 +1,13 @@
const constants = {
grays: 'https://www.grays.com',
pickles:'https://www.pickles.com.au'
}
const constants = {
grays: "https://www.grays.com",
pickles: "https://www.pickles.com.au",
allbids: "https://www.allbids.com.au",
};
export const haveHistories = [constants.grays, constants.pickles]
export const haveHistories = [
constants.grays,
constants.pickles,
constants.allbids,
];
export default constants
export default constants;

View File

@ -21,22 +21,18 @@ import {
} from "@tabler/icons-react";
import _ from "lodash";
import { useMemo, useRef, useState } from "react";
import { deleteBid, deletesBid, getBids, toggleBid } from "../apis/bid";
import {
BidModal,
ShowHistoriesBidGraysApiModal,
ShowHistoriesBidPicklesApiModal,
ShowHistoriesModal,
} from "../components/bid";
import constants, { haveHistories } from "../constant";
import { deleteBid, getBids, toggleBid } from "../apis/bid";
import { BidModal, ShowHistoriesModal } from "../components/bid";
import DeleteRowAction from "../components/bid/delete-row-action";
import { haveHistories } from "../constant";
import Table from "../lib/table/table";
import { IColumn, TRefTableFn } from "../lib/table/type";
import { useChoosesStore } from "../lib/zustand/use-chooses-store";
import { useConfirmStore } from "../lib/zustand/use-confirm";
import { mappingStatusColors } from "../system/constants";
import { IBid } from "../system/type";
import { extractDomainSmart, formatTime } from "../utils";
import DeleteRowAction from "../components/bid/delete-row-action";
import { useChoosesStore } from "../lib/zustand/use-chooses-store";
import ShowHistoriesApiModal from "../components/bid/show-histories-api/show-histories-api-modal";
export default function Bids() {
const refTableFn: TRefTableFn<IBid> = useRef({});
@ -47,12 +43,9 @@ export default function Bids() {
const { setChooses } = useChoosesStore();
const [openedHistories, historiesModel] = useDisclosure(false);
const [openedHistoriesGraysApi, historiesGraysApiModel] =
useDisclosure(false);
const [openedHistories, historiesModal] = useDisclosure(false);
const [openedHistoriesPicklesApi, historiesPicklesApiModel] =
useDisclosure(false);
const [openedHistoriesView, openedHistoriesViewModal] = useDisclosure(false);
const [openedBid, bidModal] = useDisclosure(false);
const columns: IColumn<IBid>[] = [
@ -84,7 +77,9 @@ export default function Bids() {
title: "Web",
typeFilter: "none",
renderRow(row) {
return <span>{extractDomainSmart(row.web_bid.origin_url)}</span>;
return (
<span>{extractDomainSmart(row.web_bid?.origin_url) || "None"}</span>
);
},
},
{
@ -238,28 +233,6 @@ export default function Bids() {
}}
actionsOptions={{
showMainAction: false,
actions: [
{
key: "delete",
title: "Delete",
callback: (data) => {
if (!data.length) return;
setConfirm({
title: "Delete",
message: `${data.length} will be delete`,
handleOk: async () => {
const result = await deletesBid(data);
if (!result) return;
if (refTableFn.current.fetchData) {
refTableFn.current.fetchData();
}
},
});
},
disabled: (data) => data.length <= 0,
},
],
leftActionSession: (
<Box className="flex items-end gap-2">
<Button
@ -270,7 +243,11 @@ export default function Bids() {
Add
</Button>
<DeleteRowAction onDeleted={refTableFn.current?.fetchData} />
<DeleteRowAction
onDeleted={() => {
refTableFn.current?.fetchData?.();
}}
/>
</Box>
),
}}
@ -329,29 +306,17 @@ export default function Bids() {
<Menu.Item
onClick={() => {
setClickData(row);
historiesModel.open();
historiesModal.open();
}}
leftSection={<IconHistory size={14} />}
>
Histories
</Menu.Item>
{haveHistories.includes(row?.web_bid.origin_url) && (
{haveHistories.includes(row?.web_bid?.origin_url) && (
<Menu.Item
onClick={() => {
setClickData(row);
switch (row.web_bid.origin_url) {
case constants.grays: {
historiesGraysApiModel.open();
break;
}
case constants.pickles: {
historiesPicklesApiModel.open();
break;
}
default: {
historiesGraysApiModel.open();
}
}
openedHistoriesViewModal.open();
}}
leftSection={<IconHammer size={14} />}
>
@ -405,7 +370,7 @@ export default function Bids() {
<ShowHistoriesModal
opened={openedHistories}
onClose={() => {
historiesModel.close();
historiesModal.close();
setClickData(null);
}}
data={clickData}
@ -426,28 +391,9 @@ export default function Bids() {
}}
data={clickData}
/>
{/* Grays */}
{openedHistoriesGraysApi && (
<ShowHistoriesBidGraysApiModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
setClickData(null);
}}
opened={openedHistoriesGraysApi}
onClose={() => {
historiesGraysApiModel.close();
setClickData(null);
}}
data={clickData}
/>
)}
{openedHistoriesPicklesApi && (
<ShowHistoriesBidPicklesApiModal
{openedHistoriesView && (
<ShowHistoriesApiModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
@ -457,7 +403,7 @@ export default function Bids() {
}}
opened={true}
onClose={() => {
historiesPicklesApiModel.close();
openedHistoriesViewModal.close();
setClickData(null);
}}
data={clickData}

View File

@ -1 +1 @@
{"createdAt":1747812172479}
{"createdAt":1747970028717}

View File

@ -26,7 +26,6 @@
"axios": "^1.8.3",
"bcrypt": "^5.1.1",
"bull": "^4.16.5",
"cheerio": "^1.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie": "^1.0.2",
@ -4661,7 +4660,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
"license": "ISC",
"optional": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
@ -4968,36 +4968,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/cheerio": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0",
"parse5": "^7.1.2",
"parse5-htmlparser2-tree-adapter": "^7.0.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=18.17"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
@ -5582,6 +5558,7 @@
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
@ -5598,6 +5575,7 @@
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">= 6"
},
@ -5873,6 +5851,7 @@
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"optional": true,
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
@ -5892,13 +5871,15 @@
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
"license": "BSD-2-Clause",
"optional": true
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"domelementtype": "^2.3.0"
},
@ -5914,6 +5895,7 @@
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
@ -6100,31 +6082,6 @@
"node": ">=8.10.0"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -6211,6 +6168,7 @@
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">=0.12"
},
@ -7844,6 +7802,7 @@
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
@ -10921,6 +10880,7 @@
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"optional": true,
"dependencies": {
"boolbase": "^1.0.0"
},
@ -11224,6 +11184,7 @@
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"optional": true,
"dependencies": {
"entities": "^6.0.0"
},
@ -11236,6 +11197,7 @@
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"optional": true,
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
@ -11244,23 +11206,12 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">=0.12"
},
@ -14245,15 +14196,6 @@
"node": ">=8"
}
},
"node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@ -14726,39 +14668,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",

View File

@ -42,7 +42,6 @@
"axios": "^1.8.3",
"bcrypt": "^5.1.1",
"bull": "^4.16.5",
"cheerio": "^1.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie": "^1.0.2",

View File

@ -1,59 +1,51 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import AppResponse from 'src/response/app-response';
import { Bid } from '../entities/bid.entity';
import { BidsService } from '../services/bids.service';
@Injectable()
export class GraysApi {
constructor(private readonly bidsService: BidsService){}
async getHistoriesBid(lot_id: Bid['lot_id']) {
const bid= await this.bidsService.bidsRepo.findOne({where: {lot_id, }, relations: {web_bid: true}})
try {
switch(bid.web_bid.origin_url){
// GRAYS
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
});
if (response.data && response.data?.Bids) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([])
}
// PICKLES
case 'https://www.pickles.com.au': {
const response = await axios({
url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
});
if (response.data) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([])
}
default:
return AppResponse.toResponse([])
}
} catch (error) {
return AppResponse.toResponse([]);
}
}
}
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import AppResponse from 'src/response/app-response';
import { Bid } from '../entities/bid.entity';
import { BidsService } from '../services/bids.service';
@Injectable()
export class AllBidsApi {
constructor(private readonly bidsService: BidsService) {}
async getHistoriesBid(lot_id: Bid['lot_id']) {
const bid = await this.bidsService.bidsRepo.findOne({
where: { lot_id },
relations: { web_bid: true },
});
try {
switch (bid.web_bid.origin_url) {
// GRAYS
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
});
if (response.data && response.data?.Bids) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([]);
}
// PICKLES
case 'https://www.pickles.com.au': {
const response = await axios({
url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
});
if (response.data) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([]);
}
default:
return AppResponse.toResponse([]);
}
} catch (error) {
return AppResponse.toResponse([]);
}
}
}

View File

@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import AppResponse from 'src/response/app-response';
import { Bid } from '../entities/bid.entity';
import { BidsService } from '../services/bids.service';
import * as _ from 'lodash';
@Injectable()
export class AuctionHistoresApi {
constructor(private readonly bidsService: BidsService) {}
async getHistoriesBid(lot_id: Bid['lot_id']) {
const bid = await this.bidsService.bidsRepo.findOne({
where: { lot_id },
relations: { web_bid: true, metadata: true },
});
try {
switch (bid.web_bid.origin_url) {
// GRAYS
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
});
if (response.data && response.data?.Bids) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([]);
}
// PICKLES
case 'https://www.pickles.com.au': {
const response = await axios({
url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
});
if (response.data) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([]);
}
// ALLBIDS
case 'https://www.allbids.com.au': {
const data = bid.metadata.find(
(meta) => meta.key_name === 'competor_histories',
)?.value;
const sorted = _.orderBy(data, ['amount'], ['desc']);
return AppResponse.toResponse(sorted || []);
}
default:
return AppResponse.toResponse([]);
}
} catch (error) {
return AppResponse.toResponse([]);
}
}
}

View File

@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminsModule } from '../admins/admins.module';
import { NotificationModule } from '../notification/notification.module';
import { BotTelegramApi } from './apis/bot-telegram.api';
import { GraysApi } from './apis/grays.api';
import { AuctionHistoresApi } from './apis/auction-histories.api';
import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller';
import { AdminBidsController } from './controllers/admin/admin-bids.controller';
import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller';
@ -31,6 +31,8 @@ import { TasksService } from './services/tasks.servise';
import { ConfigsService } from './services/configs.service';
import { Config } from './entities/configs.entity';
import { AdminConfigsController } from './controllers/admin/admin-configs.controller';
import { BidMetadatasService } from './services/bid-metadatas.service';
import { BidMetadata } from './entities/bid-metadata.entity';
@Module({
imports: [
@ -41,6 +43,7 @@ import { AdminConfigsController } from './controllers/admin/admin-configs.contro
WebBid,
SendMessageHistory,
Config,
BidMetadata,
]),
// AuthModule,
AdminsModule,
@ -66,12 +69,13 @@ import { AdminConfigsController } from './controllers/admin/admin-configs.contro
OutBidLogsService,
WebBidsService,
BotTelegramApi,
GraysApi,
AuctionHistoresApi,
SendMessageHistoriesService,
ImapService,
DashboardService,
TasksService,
ConfigsService,
BidMetadatasService,
],
exports: [
BotTelegramApi,

View File

@ -4,13 +4,13 @@ import { CreateBidDto } from '../../dto/bid/create-bid.dto';
import { BidHistoriesService } from '../../services/bid-histories.service';
import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
import { Bid } from '../../entities/bid.entity';
import { GraysApi } from '../../apis/grays.api';
import { AuctionHistoresApi } from '../../apis/auction-histories.api';
@Controller('admin/bid-histories')
export class AdminBidHistoriesController {
constructor(
private readonly bidHistoriesService: BidHistoriesService,
private readonly graysApi: GraysApi,
private readonly auctionHistoresApi: AuctionHistoresApi,
) {}
@Post()
@ -20,6 +20,6 @@ export class AdminBidHistoriesController {
@Get('detail/:lot_id')
async getBidHistories(@Param('lot_id') lot_id: Bid['lot_id']) {
return await this.graysApi.getHistoriesBid(lot_id);
return await this.auctionHistoresApi.getHistoriesBid(lot_id);
}
}

View File

@ -1,4 +1,4 @@
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
export class ClientUpdateBidDto {
@IsString()
@ -24,4 +24,8 @@ export class ClientUpdateBidDto {
@IsNumber()
@IsOptional()
reserve_price: number;
@IsObject()
@IsOptional()
metadata: Record<string, any>;
}

View File

@ -0,0 +1,25 @@
import {
Column,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';
import { Bid } from './bid.entity';
import { Timestamp } from './timestamp';
@Entity('bid_metadata')
@Unique(['key_name', 'bid'])
export class BidMetadata extends Timestamp {
@PrimaryGeneratedColumn('increment')
id: number;
@Column()
key_name: string;
@Column({ type: 'json' })
value: string;
@ManyToOne(() => Bid, (bid) => bid.metadata, { onDelete: 'CASCADE' })
bid: Bid;
}

View File

@ -9,6 +9,7 @@ import { Timestamp } from './timestamp';
import { BidHistory } from './bid-history.entity';
import { WebBid } from './wed-bid.entity';
import { SendMessageHistory } from './send-message-histories.entity';
import { BidMetadata } from './bid-metadata.entity';
@Entity('bids')
export class Bid extends Timestamp {
@ -69,4 +70,7 @@ export class Bid extends Timestamp {
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
web_bid: WebBid;
@OneToMany(() => BidMetadata, (metadata) => metadata.bid)
metadata: BidMetadata[];
}

View File

@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BidMetadata } from '../entities/bid-metadata.entity';
import { Bid } from '../entities/bid.entity';
@Injectable()
export class BidMetadatasService {
constructor(
@InjectRepository(BidMetadata)
readonly bidMetadataRepo: Repository<BidMetadata>,
) {}
async upsert(data: Record<string, any>, bid: Bid) {
const existingMetadata = await this.bidMetadataRepo.find({
where: { bid: { id: bid.id } },
});
const existingMap = new Map(
existingMetadata.map((item) => [item.key_name, item]),
);
const toSave: BidMetadata[] = [];
for (const [key, value] of Object.entries(data)) {
const existing = existingMap.get(key);
if (existing) {
existing.value = value;
toSave.push(existing);
} else {
toSave.push(
this.bidMetadataRepo.create({
key_name: key,
value,
bid: { id: bid.id },
}),
);
}
}
await this.bidMetadataRepo.save(toSave);
return true;
}
}

View File

@ -32,6 +32,7 @@ import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
import { Constant } from '../utils/constant';
import { Event } from '../utils/events';
import { WebBidsService } from './web-bids.service';
import { BidMetadatasService } from './bid-metadatas.service';
@Injectable()
export class BidsService {
@ -41,8 +42,9 @@ export class BidsService {
@InjectRepository(BidHistory)
readonly bidHistoriesRepo: Repository<BidHistory>,
private readonly webBidsService: WebBidsService,
private eventEmitter: EventEmitter2,
private notificationService: NotificationService,
private readonly eventEmitter: EventEmitter2,
private readonly notificationService: NotificationService,
private readonly bidMetadatasService: BidMetadatasService,
) {}
async index(query: PaginateQuery) {
@ -208,12 +210,12 @@ export class BidsService {
async clientUpdate(
id: Bid['id'],
{ close_time, model, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
{ close_time, model, metadata, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật
) {
// Tìm kiếm phiên đấu giá trong database theo id
const bid = await this.bidsRepo.findOne({
where: { id },
relations: { histories: true, web_bid: true },
relations: { histories: true, web_bid: true, metadata: true },
order: {
histories: {
price: 'DESC',
@ -288,6 +290,10 @@ export class BidsService {
updated_at: new Date(), // Cập nhật timestamp
});
if (metadata) {
await this.bidMetadatasService.upsert(metadata, bid);
}
// Phát sự kiện cập nhật toàn bộ danh sách đấu giá
this.emitAllBidEvent();

View File

@ -15,6 +15,7 @@ import { Queue } from 'bull';
export class MailsService {
constructor(
private readonly mailerService: MailerService,
@InjectQueue('mail-queue') private mailQueue: Queue,
) {}
@ -52,6 +53,8 @@ export class MailsService {
}
generateProductTableHTML(products: ScrapItem[]): string {
const from = process.env.MAIL_USER || 'no-reply@example.com';
if (!products.length) {
return `
<!DOCTYPE html>
@ -64,6 +67,7 @@ export class MailsService {
<body style="font-family: sans-serif; background: #f8f9fa; padding: 20px;">
<h2 style="text-align: center; color: #333;">Product Listing</h2>
<p style="text-align: center; color: #666;">No matching products found for your keywords today.</p>
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
</body>
</html>
`;
@ -109,6 +113,7 @@ export class MailsService {
</tbody>
</table>
</div>
<p style="text-align: center; color: #999; font-size: 12px; margin-top: 40px;">From: ${from}</p>
</body>
</html>
`;
@ -122,6 +127,7 @@ export class MailsService {
const max = `$${bid.max_price}`;
const submitted = `$${bid.max_price}`;
const nextBid = bid.max_price + bid.plus_price;
const from = process.env.MAIL_USER || 'no-reply@example.com';
const cardStyle = `
max-width: 600px;
@ -146,15 +152,16 @@ export class MailsService {
switch (bid.status) {
case 'biding':
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#2c7a7b')}"> Auto Bid Started</h2>
${renderRow('Title', title)}
${renderRow('Max', max)}
${renderRow('End time', endTime)}
${renderRow('Competitor', competitor)}
${renderRow('Bid submitted', submitted)}
</div>
`;
<div style="${cardStyle}">
<h2 style="${headerStyle('#2c7a7b')}"> Auto Bid Started</h2>
${renderRow('Title', title)}
${renderRow('Max', max)}
${renderRow('End time', endTime)}
${renderRow('Competitor', competitor)}
${renderRow('Bid submitted', submitted)}
${renderRow('From', from)}
</div>
`;
case 'out-bid': {
const overLimit = bid.current_price >= nextBid;
@ -163,59 +170,64 @@ export class MailsService {
if (isTimeReached(bid.close_time)) {
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}"> Auction Ended</h2>
${renderRow('Title', title)}
${renderRow('End time', endTime)}
${renderRow('Final price', competitor)}
</div>
`;
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}"> Auction Ended</h2>
${renderRow('Title', title)}
${renderRow('End time', endTime)}
${renderRow('Final price', competitor)}
${renderRow('From', from)}
</div>
`;
}
if (overLimit || belowReserve) {
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#dd6b20')}"> Outbid (${timeExtended})</h2>
${renderRow('Title', title)}
${renderRow('Competitor', competitor)}
${renderRow('Max', max)}
${renderRow('Next bid at', `$${nextBid}`)}
${renderRow('End time', endTime)}
<p style="color:#c05621; font-weight: 600;"> Current bid exceeds your max bid.</p>
</div>
`;
}
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#e53e3e')}">🛑 Auction Canceled (${timeExtended})</h2>
<h2 style="${headerStyle('#dd6b20')}"> Outbid (${timeExtended})</h2>
${renderRow('Title', title)}
${renderRow('Competitor', competitor)}
${renderRow('Max', max)}
${renderRow('Next bid at', `$${nextBid}`)}
${renderRow('End time', endTime)}
<p style="color:#9b2c2c; font-weight: 600;">🛑 Auction has been canceled.</p>
${renderRow('From', from)}
<p style="color:#c05621; font-weight: 600;"> Current bid exceeds your max bid.</p>
</div>
`;
}
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#e53e3e')}">🛑 Auction Canceled (${timeExtended})</h2>
${renderRow('Title', title)}
${renderRow('Competitor', competitor)}
${renderRow('Max', max)}
${renderRow('Next bid at', `$${nextBid}`)}
${renderRow('End time', endTime)}
${renderRow('From', from)}
<p style="color:#9b2c2c; font-weight: 600;">🛑 Auction has been canceled.</p>
</div>
`;
}
case 'win-bid':
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#2b6cb0')}">🎉 You Won!</h2>
${renderRow('Title', title)}
${renderRow('Price won', `$${bid.current_price}`)}
${renderRow('Max', max)}
</div>
`;
<div style="${cardStyle}">
<h2 style="${headerStyle('#2b6cb0')}">🎉 You Won!</h2>
${renderRow('Title', title)}
${renderRow('Price won', `$${bid.current_price}`)}
${renderRow('Max', max)}
${renderRow('From', from)}
</div>
`;
default:
return `
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}"> Unknown Status</h2>
${renderRow('Title', title)}
</div>
`;
<div style="${cardStyle}">
<h2 style="${headerStyle('#718096')}"> Unknown Status</h2>
${renderRow('Title', title)}
${renderRow('From', from)}
</div>
`;
}
}
@ -227,6 +239,7 @@ export class MailsService {
const max = `$${bid.max_price}`;
const submitted = `$${bid.max_price}`;
const maxReached = bid.max_price <= bid.max_price;
const from = process.env.MAIL_USER || 'no-reply@example.com';
return `
<!DOCTYPE html>
@ -300,6 +313,10 @@ export class MailsService {
<th>End Time</th>
<td>${endTime}</td>
</tr>
<tr>
<th>From</th>
<td>${from}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -6,7 +6,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import * as moment from 'moment';
import { Between } from 'typeorm';
import { ScrapConfigsService } from './scrap-config.service';
import { ScrapItemsService } from './scrap-item-config.service';
@Injectable()
@ -87,7 +86,7 @@ export class TasksService {
}
}
@Cron('0 2 * * *')
@Cron('58 5 * * *')
async handleScraps() {
const processName = 'scrape-data-keyword';
await this.runProcessAndSendReport(processName);

View File

@ -59,6 +59,48 @@ export class AllbidsProductBid extends ProductBid {
}
}
async getHistoriesData() {
if (!this.page_context) return;
try {
// Chờ cho Angular load (có thể tùy chỉnh thời gian nếu cần)
await this.page_context.waitForFunction(
() => window.angular !== undefined
);
const historiesData = await this.page_context.evaluate(() => {
let data = null;
const elements = document.querySelectorAll(".ng-scope");
for (let i = 0; i < elements.length; i++) {
try {
const scope = angular.element(elements[i]).scope();
if (scope?.bidHistory) {
data = scope.bidHistory;
break;
}
// Thử lấy từ $parent nếu không thấy
if (scope?.$parent?.bidHistory) {
data = scope.$parent.bidHistory;
break;
}
} catch (e) {
// Angular element có thể lỗi nếu phần tử không hợp lệ
continue;
}
}
return data;
});
return historiesData;
} catch (error) {
console.log(
`[${this.id}] Error in waitForApiResponse: ${error?.message}`
);
}
}
async handleUpdateBid({
lot_id,
close_time,
@ -66,6 +108,7 @@ export class AllbidsProductBid extends ProductBid {
current_price,
reserve_price,
model,
metadata,
}) {
const response = await updateBid(this.id, {
lot_id,
@ -74,6 +117,7 @@ export class AllbidsProductBid extends ProductBid {
current_price,
reserve_price: Number(reserve_price) || 0,
model,
metadata,
});
if (response) {
@ -122,6 +166,8 @@ export class AllbidsProductBid extends ProductBid {
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
const result = await this.waitForApiResponse();
const historiesData = await this.getHistoriesData();
// 📌 Nếu không có dữ liệu trả về thì dừng
if (!result) {
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
@ -140,6 +186,9 @@ export class AllbidsProductBid extends ProductBid {
: null,
// close_time: close_time && !this.close_time ? String(close_time) : null, // test
name: result?.aucTitle || null,
metadata: {
competor_histories: historiesData,
},
},
["close_time"]
);
@ -233,12 +282,10 @@ export class AllbidsProductBid extends ProductBid {
const data = await this.submitBid();
console.log({ data });
await this.page_context.reload({ waitUntil: "networkidle0" });
const { aucUserMaxBid } = await this.waitForApiResponse();
console.log(`📡 [${this.id}] API Response received:`, lotData);
console.log(`📡 [${this.id}] API Response received:`, { aucUserMaxBid });
// 📌 Kiểm tra trạng thái đấu giá từ API
if (aucUserMaxBid == this.max_price) {