From 65ae7da6e6894ecbae6ac8b4385dbbdfe99b344d Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 5 May 2025 10:05:12 +0700 Subject: [PATCH] pickxel and fix login --- auto-bid-admin/src/apis/web-bid.ts | 152 ++++---- .../show-histories-bid-pickles-api-modal.tsx | 4 + .../components/bid/show-histories-modal.tsx | 2 +- .../src/components/web-bid/web-bid-modal.tsx | 260 +++++++++----- auto-bid-admin/src/system/type/index.ts | 40 ++- auto-bid-admin/src/utils/index.ts | 31 ++ auto-bid-server/bot-data/metadata.json | 6 +- .../src/modules/bids/apis/grays.api.ts | 13 + .../bids/dto/web-bid/update-web-bid.ts | 12 +- .../modules/bids/entities/wed-bid.entity.ts | 6 + .../src/modules/bids/services/bids.service.ts | 2 +- .../modules/bids/services/web-bids.service.ts | 2 +- auto-bid-tool/index.js | 210 ++++++++++-- auto-bid-tool/models/api-bid.js | 220 +++++++----- .../langtons.com.au/langtons-api-bid.js | 2 + .../langtons.com.au/langtons-product-bid.js | 24 +- .../lawsons.com.au/lawsons-product-bid.js | 7 +- auto-bid-tool/system/utils.js | 324 +++++++++++------- 18 files changed, 886 insertions(+), 431 deletions(-) diff --git a/auto-bid-admin/src/apis/web-bid.ts b/auto-bid-admin/src/apis/web-bid.ts index 175c83c..8faac2e 100644 --- a/auto-bid-admin/src/apis/web-bid.ts +++ b/auto-bid-admin/src/apis/web-bid.ts @@ -1,92 +1,110 @@ -import { generateNestParams, handleError, handleSuccess } from '.'; -import axios from '../lib/axios'; -import { IWebBid } from '../system/type'; -import { removeFalsyValues } from '../utils'; +import { generateNestParams, handleError, handleSuccess } from "."; +import axios from "../lib/axios"; +import { IWebBid } from "../system/type"; +import { removeFalsyValues } from "../utils"; -const BASE_URL = 'web-bids'; +const BASE_URL = "web-bids"; export const getWebBids = async (params: Record) => { - return await axios({ - url: BASE_URL, - params: generateNestParams(params), - withCredentials: true, - method: 'GET', - }); + return await axios({ + url: BASE_URL, + params: generateNestParams(params), + withCredentials: true, + method: "GET", + }); }; -export const createWebBid = async (bid: Omit) => { - const newData = removeFalsyValues(bid); +export const createWebBid = async ( + bid: Omit +) => { + const newData = removeFalsyValues(bid); - try { - const { data } = await axios({ - url: BASE_URL, - withCredentials: true, - method: 'POST', - data: newData, - }); + try { + const { data } = await axios({ + url: BASE_URL, + withCredentials: true, + method: "POST", + data: newData, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const updateWebBid = async (bid: Partial) => { - const { url, password, username, origin_url, active } = removeFalsyValues(bid, ['active']); + const { + url, + password, + username, + origin_url, + active, + arrival_offset_seconds, + // early_login_seconds + } = removeFalsyValues(bid, ["active"]); - try { - const { data } = await axios({ - url: `${BASE_URL}/` + bid.id, - withCredentials: true, - method: 'PUT', - data: { url, password, username, origin_url, active }, - }); + try { + const { data } = await axios({ + url: `${BASE_URL}/` + bid.id, + withCredentials: true, + method: "PUT", + data: { + url, + password, + username, + origin_url, + active, + arrival_offset_seconds, + // early_login_seconds + }, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const deleteWebBid = async (web: IWebBid) => { - try { - const { data } = await axios({ - url: `${BASE_URL}/` + web.id, - withCredentials: true, - method: 'DELETE', - }); + try { + const { data } = await axios({ + url: `${BASE_URL}/` + web.id, + withCredentials: true, + method: "DELETE", + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const deletesWebBid = async (web: IWebBid[]) => { - const ids = web.reduce((prev, cur) => { - prev.push(cur.id); - return prev; - }, [] as number[]); - try { - const { data } = await axios({ - url: `${BASE_URL}/deletes`, - withCredentials: true, - method: 'POST', - data: { - ids, - }, - }); + const ids = web.reduce((prev, cur) => { + prev.push(cur.id); + return prev; + }, [] as number[]); + try { + const { data } = await axios({ + url: `${BASE_URL}/deletes`, + withCredentials: true, + method: "POST", + data: { + ids, + }, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; diff --git a/auto-bid-admin/src/components/bid/show-histories-bid-pickles-api-modal.tsx b/auto-bid-admin/src/components/bid/show-histories-bid-pickles-api-modal.tsx index ff3b5c9..67d68c6 100644 --- a/auto-bid-admin/src/components/bid/show-histories-bid-pickles-api-modal.tsx +++ b/auto-bid-admin/src/components/bid/show-histories-bid-pickles-api-modal.tsx @@ -20,7 +20,11 @@ export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...pr {element['bidderAnonName']} {element['actualBid']} +<<<<<<< HEAD {formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())} +======= + {formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString(), 'HH:mm:ss DD/MM/YYYY')} +>>>>>>> 26b10a7 (pickxel and fix login) )); }, [histories]); diff --git a/auto-bid-admin/src/components/bid/show-histories-modal.tsx b/auto-bid-admin/src/components/bid/show-histories-modal.tsx index 8708805..3cddae8 100644 --- a/auto-bid-admin/src/components/bid/show-histories-modal.tsx +++ b/auto-bid-admin/src/components/bid/show-histories-modal.tsx @@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH {element.id} {element.price} - {formatTime(element.created_at, 'DD/MM/YYYY HH:MM')} + {formatTime(new Date(element.created_at).toUTCString(), 'HH:mm:ss DD/MM/YYYY')} )); diff --git a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx index fa072fd..42190b1 100644 --- a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx +++ b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx @@ -1,118 +1,196 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core'; -import { useForm, zodResolver } from '@mantine/form'; -import _ from 'lodash'; -import { useEffect, useRef, useState } from 'react'; -import { z } from 'zod'; -import { createWebBid, updateWebBid } from '../../apis/web-bid'; -import { useConfirmStore } from '../../lib/zustand/use-confirm'; -import { IWebBid } from '../../system/type'; -import { extractDomain } from '../../utils'; +import { + Button, + LoadingOverlay, + Modal, + ModalProps, + NumberInput, + TextInput, +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import _ from "lodash"; +import { useEffect, useRef, useState } from "react"; +import { z } from "zod"; +import { createWebBid, updateWebBid } from "../../apis/web-bid"; +import { useConfirmStore } from "../../lib/zustand/use-confirm"; +import { IWebBid } from "../../system/type"; +import { extractDomain } from "../../utils"; export interface IWebBidModelProps extends ModalProps { - data: IWebBid | null; - onUpdated?: () => void; + data: IWebBid | null; + onUpdated?: () => void; } const schema = { - url: z.string({ message: 'Url is required' }).url('Invalid url format'), + url: z.string({ message: "Url is required" }).url("Invalid url format"), + arrival_offset_seconds: z + .number({ message: "Arrival offset seconds is required" }) + .refine((val) => val >= 60, { + message: "Arrival offset seconds must be at least 60 seconds (1 minute)", + }), + early_login_seconds: z + .number({ message: "Early login seconds is required" }) + .refine((val) => val >= 600, { + message: "Early login seconds must be at least 600 seconds (10 minute)", + }), }; -export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelProps) { - const form = useForm({ - validate: zodResolver(z.object(schema)), - }); +export default function WebBidModal({ + data, + onUpdated, + ...props +}: IWebBidModelProps) { + const form = useForm({ + validate: zodResolver(z.object(schema)), + }); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - const prevData = useRef(data); + const prevData = useRef(data); - const { setConfirm } = useConfirmStore(); + const { setConfirm } = useConfirmStore(); - const handleSubmit = async (values: typeof form.values) => { - if (data) { - setConfirm({ - title: 'Update ?', - message: `This web will be update`, - handleOk: async () => { - setLoading(true); - const result = await updateWebBid(values); - setLoading(false); + const handleSubmit = async (values: typeof form.values) => { + if (data) { + setConfirm({ + title: "Update ?", + message: `This web will be update`, + handleOk: async () => { + setLoading(true); + console.log( + "%csrc/components/web-bid/web-bid-modal.tsx:54 values", + "color: #007acc;", + values + ); + const result = await updateWebBid(values); + setLoading(false); - if (!result) return; + if (!result) return; - props.onClose(); + props.onClose(); - if (onUpdated) { - onUpdated(); - } - }, - okButton: { - color: 'blue', - value: 'Update', - }, - }); - } else { - const { url, origin_url } = values; + if (onUpdated) { + onUpdated(); + } + }, + okButton: { + color: "blue", + value: "Update", + }, + }); + } else { + const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values; - setLoading(true); - const result = await createWebBid({ url, origin_url } as IWebBid); - setLoading(false); + setLoading(true); + const result = await createWebBid({ + url, + origin_url, + arrival_offset_seconds, + early_login_seconds + } as IWebBid); + setLoading(false); - if (!result) return; + if (!result) return; - props.onClose(); + props.onClose(); - if (onUpdated) { - onUpdated(); - } - } - }; + if (onUpdated) { + onUpdated(); + } + } + }; - useEffect(() => { - form.reset(); - if (!data) return; + useEffect(() => { + form.reset(); + if (!data) return; - form.setValues(data); + form.setValues(data); - prevData.current = data; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]); + prevData.current = data; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); - useEffect(() => { - if (!props.opened) { - form.reset(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.opened]); + useEffect(() => { + if (!props.opened) { + form.reset(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.opened]); - useEffect(() => { - if (form.values?.url) { - form.setFieldValue('origin_url', extractDomain(form.values.url)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values]); + useEffect(() => { + if (form.values?.url) { + form.setFieldValue("origin_url", extractDomain(form.values.url)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form.values]); - return ( - Web} - centered + return ( + Web} + centered + > +
+ + + + + {/* */} + + + - - - - -
- ); + +
+ ); } diff --git a/auto-bid-admin/src/system/type/index.ts b/auto-bid-admin/src/system/type/index.ts index b8a7b85..28f68ed 100644 --- a/auto-bid-admin/src/system/type/index.ts +++ b/auto-bid-admin/src/system/type/index.ts @@ -18,24 +18,7 @@ export interface ITimestamp { updated_at: string; } -export interface IBid extends ITimestamp { - id: number; - max_price: number; - reserve_price: number; - current_price: number; - name: string | null; - quantity: number; - url: string; - model: string; - lot_id: string; - plus_price: number; - close_time: string | null; - start_bid_time: string | null; - first_bid: boolean; - status: 'biding' | 'out-bid' | 'win-bid'; - histories: IHistory[]; - web_bid: IWebBid; -} + export interface IHistory extends ITimestamp { id: number; @@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp { username: string | null; password: string | null; active: boolean; + arrival_offset_seconds: number; + early_login_seconds: number; children: IBid[]; } +export interface IBid extends ITimestamp { + id: number; + max_price: number; + reserve_price: number; + current_price: number; + name: string | null; + quantity: number; + url: string; + model: string; + lot_id: string; + plus_price: number; + close_time: string | null; + start_bid_time: string | null; + first_bid: boolean; + status: 'biding' | 'out-bid' | 'win-bid'; + histories: IHistory[]; + web_bid: IWebBid; +} + export interface IPermission extends ITimestamp { id: number; name: string; diff --git a/auto-bid-admin/src/utils/index.ts b/auto-bid-admin/src/utils/index.ts index bcc8383..06b8e46 100644 --- a/auto-bid-admin/src/utils/index.ts +++ b/auto-bid-admin/src/utils/index.ts @@ -3,6 +3,10 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import moment from "moment"; +<<<<<<< HEAD +======= +import { IWebBid } from "../system/type"; +>>>>>>> 26b10a7 (pickxel and fix login) export function cn(...args: ClassValue[]) { return twMerge(clsx(args)); } @@ -150,4 +154,31 @@ export function stringToColor(str: string): string { const hash = hashStringToInt(str); const index = hash % colorPalette.length; return colorPalette[index]; +<<<<<<< HEAD +======= +} + +export function findEarlyLoginTime(webBid: IWebBid): string | null { + const now = new Date(); + + // Bước 1: Lọc ra những bid có close_time hợp lệ + const validChildren = webBid.children.filter(child => child.close_time); + + if (validChildren.length === 0) return null; + + // Bước 2: Tìm bid có close_time gần hiện tại nhất + const closestBid = validChildren.reduce((closest, current) => { + const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime()); + const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime()); + return currentDiff < closestDiff ? current : closest; + }); + + if (!closestBid.close_time) return null; + + // Bước 3: Tính toán thời gian login sớm + const closeTime = new Date(closestBid.close_time); + closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_login_seconds || 0)); + + return closeTime.toISOString(); +>>>>>>> 26b10a7 (pickxel and fix login) } diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index 4af7bd9..93d37ea 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1,5 @@ -{"createdAt":1745827424853} \ No newline at end of file +<<<<<<< HEAD +{"createdAt":1745827424853} +======= +{"createdAt":1746413672600} +>>>>>>> 26b10a7 (pickxel and fix login) diff --git a/auto-bid-server/src/modules/bids/apis/grays.api.ts b/auto-bid-server/src/modules/bids/apis/grays.api.ts index d8a9994..e385db2 100644 --- a/auto-bid-server/src/modules/bids/apis/grays.api.ts +++ b/auto-bid-server/src/modules/bids/apis/grays.api.ts @@ -21,6 +21,10 @@ export class GraysApi { switch(bid.web_bid.origin_url){ +<<<<<<< HEAD +======= + // GRAYS +>>>>>>> 26b10a7 (pickxel and fix login) case 'https://www.grays.com': { const response = await axios({ url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`, @@ -32,6 +36,11 @@ export class GraysApi { return AppResponse.toResponse([]) } +<<<<<<< HEAD +======= + + // PICKLES +>>>>>>> 26b10a7 (pickxel and fix login) case 'https://www.pickles.com.au': { const response = await axios({ @@ -39,7 +48,11 @@ export class GraysApi { }); if (response.data) { +<<<<<<< HEAD return AppResponse.toResponse(response.data); +======= + return AppResponse.toResponse(response.data.Bids); +>>>>>>> 26b10a7 (pickxel and fix login) } return AppResponse.toResponse([]) diff --git a/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts b/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts index a0bcff4..9acee56 100644 --- a/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts +++ b/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator'; export class UpdateWebBidDto { @IsUrl() @@ -9,6 +9,16 @@ export class UpdateWebBidDto { @IsOptional() url: string; + @IsNumber() + @Min(60) + @IsOptional() + arrival_offset_seconds: number; + + @IsNumber() + @Min(600) + @IsOptional() + early_login_seconds: number; + @IsString() @IsOptional() username: string; diff --git a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts index 5111002..4fce0fd 100644 --- a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts @@ -17,6 +17,12 @@ export class WebBid extends Timestamp { @Column({ default: null, nullable: true }) username: string; + @Column({ default: 300 }) + arrival_offset_seconds: number; + + @Column({ default: 600 }) + early_login_seconds: number; + @Column({ default: null, nullable: true }) @Exclude() password: string; diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index 41c89ef..c1da5de 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -228,7 +228,7 @@ export class BidsService { if (!bid.close_time && !bid.start_bid_time) { // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng // bid.start_bid_time = new Date().toUTCString(); - bid.start_bid_time = subtractMinutes(close_time, 5); + bid.start_bid_time = subtractMinutes(close_time, bid.web_bid.arrival_offset_seconds/ 60); } // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc) diff --git a/auto-bid-server/src/modules/bids/services/web-bids.service.ts b/auto-bid-server/src/modules/bids/services/web-bids.service.ts index 96e028f..9a48eb1 100644 --- a/auto-bid-server/src/modules/bids/services/web-bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/web-bids.service.ts @@ -80,7 +80,7 @@ export class WebBidsService { async emitAccountUpdate(id: WebBid['id']) { const data = await this.webBidRepo.findOne({ where: { id, children: { status: 'biding' } }, - relations: { children: true }, + relations: { children: { web_bid: true } }, }); this.eventEmitter.emit(Event.WEB_UPDATED, data || null); diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index a098dda..c64bf3b 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -10,7 +10,12 @@ import { } from "./service/app-service.js"; import browser from "./system/browser.js"; import configs from "./system/config.js"; -import { delay, isTimeReached, safeClosePage } from "./system/utils.js"; +import { + delay, + findEarlyLoginTime, + isTimeReached, + safeClosePage, +} from "./system/utils.js"; import { updateLoginStatus } from "./system/apis/bid.js"; global.IS_CLEANING = true; @@ -53,6 +58,59 @@ const handleUpdateProductTabs = (data) => { MANAGER_BIDS = newDataManager; }; +const addProductTab = (data) => { + if ( + typeof data !== "object" || + data === null || + !Array.isArray(data.children) + ) { + console.warn("Data must be an object with a children array"); + return; + } + + const { children, ...web } = data; + + if (children.length === 0) { + console.warn( + `⚠️ No children found for bid id ${web.id}, skipping addProductTab` + ); + return; + } + + const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid])); + const prevApiBid = managerBidMap.get(web.id); + + if (prevApiBid) { + // Cập nhật + const updatedChildren = prevApiBid.children; + + children.forEach((newChild) => { + const existingChildIndex = updatedChildren.findIndex( + (c) => c.id === newChild.id + ); + + if (existingChildIndex !== -1) { + updatedChildren[existingChildIndex].setNewData(newChild); + } else { + updatedChildren.push(createBidProduct(web, newChild)); + } + }); + + prevApiBid.setNewData({ ...web, children: updatedChildren }); + } else { + // Tạo mới + const newChildren = children.map((item) => createBidProduct(web, item)); + const newApiBid = createApiBid({ ...web, children: newChildren }); + + MANAGER_BIDS.push(newApiBid); + + console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", { + MANAGER_BIDS, + children: newChildren, + }); + } +}; + const tracking = async () => { console.log("🚀 Tracking process started..."); @@ -68,6 +126,20 @@ const tracking = async () => { }) ); + await Promise.allSettled( + MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => { + console.log(`🎧 Listening to events for close login: ${apiBid.id}`); + return (apiBid.onCloseLogin = (data) => { + // Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại + MANAGER_BIDS = MANAGER_BIDS.filter( + (item) => item.id !== data.id && item.type !== data.type + ); + + addProductTab(data); + }); + }) + ); + Promise.allSettled( productTabs.map(async (productTab) => { console.log(`📌 Processing Product ID: ${productTab.id}`); @@ -159,6 +231,72 @@ const tracking = async () => { } }; +// const clearLazyTab = async () => { +// if (!global.IS_CLEANING) { +// console.log("🚀 Cleaning flag is OFF. Proceeding with operation."); +// return; +// } + +// if (!browser) { +// console.warn("⚠️ Browser is not available or disconnected."); +// return; +// } + +// try { +// const pages = await browser.pages(); + +// // Lấy danh sách URL từ flattenedArray +// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ +// item.url, +// ...item.children.map((child) => child.url), +// ]).filter(Boolean); // Lọc bỏ null hoặc undefined + +// console.log( +// "🔍 Page URLs:", +// pages.map((page) => page.url()) +// ); + +// for (const page of pages) { +// const pageUrl = page.url(); + +// if (!pageUrl || pageUrl === "about:blank") continue; + +// if (!activeUrls.includes(pageUrl)) { +// if (!page.isClosed() && browser.isConnected()) { +// try { +// const bidData = MANAGER_BIDS.filter((item) => item.page_context) +// .map((i) => ({ +// current_url: i.page_context.url(), +// data: i, +// })) +// .find((j) => j.current_url === pageUrl); + +// console.log(bidData); + +// if (bidData && bidData.data) { +// await safeClosePage(bidData.data); +// } else { +// // 👇 Wrap close with timeout + error catch +// await Promise.race([ +// page.close(), +// new Promise((_, reject) => +// setTimeout(() => reject(new Error("Close timeout")), 3000) +// ), +// ]); +// } + +// console.log(`🛑 Closing unused tab: ${pageUrl}`); +// } catch (err) { +// console.warn(`⚠️ Error closing tab ${pageUrl}: ${err.message}`); +// } +// } +// } +// } +// } catch (err) { +// console.error("❌ Error in clearLazyTab:", err.message); +// } +// }; + const clearLazyTab = async () => { if (!global.IS_CLEANING) { console.log("🚀 Cleaning flag is OFF. Proceeding with operation."); @@ -172,47 +310,53 @@ const clearLazyTab = async () => { try { const pages = await browser.pages(); + console.log("🔍 Found pages:", pages.length); - // Lấy danh sách URL từ flattenedArray const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ item.url, ...item.children.map((child) => child.url), - ]).filter(Boolean); // Lọc bỏ null hoặc undefined - - console.log( - "🔍 Page URLs:", - pages.map((page) => page.url()) - ); + ]).filter(Boolean); for (const page of pages) { - const pageUrl = page.url(); + try { + if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua - // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL - if (!pageUrl || pageUrl === "about:blank") continue; + const pageUrl = page.url(); - if (!activeUrls.includes(pageUrl)) { - if (!page.isClosed() && browser.isConnected()) { + if (!pageUrl || pageUrl === "about:blank") continue; + if (activeUrls.includes(pageUrl)) continue; + + page.removeAllListeners(); + + console.log(`🛑 Unused page detected: ${pageUrl}`); + + const bidData = MANAGER_BIDS.filter((item) => item.page_context) + .map((i) => ({ + current_url: i.page_context.url(), + data: i, + })) + .find((j) => j.current_url === pageUrl); + + if (bidData && bidData.data) { + await safeClosePage(bidData.data); + } else { try { - const bidData = MANAGER_BIDS.filter((item) => item.page_context) - .map((i) => ({ - current_url: i.page_context.url(), - data: i, - })) - .find((j) => j.current_url === pageUrl); - - console.log(bidData); - - if (bidData && bidData.data) { - await safeClosePage(bidData.data); - } else { - await page.close(); - } - - console.log(`🛑 Closing unused tab: ${pageUrl}`); - } catch (err) { - console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`); + await Promise.race([ + page.close(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Close timeout")), 3000) + ), + ]); + } catch (closeErr) { + console.warn( + `⚠️ Error closing page ${pageUrl}: ${closeErr.message}` + ); } } + + console.log(`✅ Closed page: ${pageUrl}`); + } catch (pageErr) { + console.warn(`⚠️ Error handling page: ${pageErr.message}`); } } } catch (err) { @@ -343,6 +487,10 @@ const trackingLoginStatus = async () => { await safeClosePage(tab); + MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id); + + addProductTab(data); + global.IS_CLEANING = true; } else { console.log("⚠️ No profile found to delete."); diff --git a/auto-bid-tool/models/api-bid.js b/auto-bid-tool/models/api-bid.js index 548aa06..7d82433 100644 --- a/auto-bid-tool/models/api-bid.js +++ b/auto-bid-tool/models/api-bid.js @@ -1,106 +1,144 @@ -import * as fs from 'fs'; -import path from 'path'; -import BID_TYPE from '../system/bid-type.js'; -import browser from '../system/browser.js'; -import CONSTANTS from '../system/constants.js'; -import { getPathProfile, sanitizeFileName } from '../system/utils.js'; -import { Bid } from './bid.js'; +import * as fs from "fs"; +import path from "path"; +import BID_TYPE from "../system/bid-type.js"; +import browser from "../system/browser.js"; +import CONSTANTS from "../system/constants.js"; +import { + findEarlyLoginTime, + getPathProfile, + isTimeReached, + sanitizeFileName, +} from "../system/utils.js"; +import { Bid } from "./bid.js"; export class ApiBid extends Bid { - id; - account; - children = []; - children_processing = []; - created_at; - updated_at; - origin_url; - active; - browser_context; - username; - password; + id; + account; + children = []; + children_processing = []; + created_at; + updated_at; + origin_url; + active; + browser_context; + username; + password; - constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) { - super(BID_TYPE.API_BID, url); + constructor({ + url, + username, + password, + id, + children, + created_at, + updated_at, + origin_url, + active, + }) { + super(BID_TYPE.API_BID, url); - this.created_at = created_at; - this.updated_at = updated_at; - this.children = children; - this.origin_url = origin_url; - this.active = active; - this.username = username; - this.password = password; - this.id = id; + this.created_at = created_at; + this.updated_at = updated_at; + this.children = children; + this.origin_url = origin_url; + this.active = active; + this.username = username; + this.password = password; + this.id = id; + } + + setNewData({ + url, + username, + password, + id, + children, + created_at, + updated_at, + origin_url, + active, + }) { + this.created_at = created_at; + this.updated_at = updated_at; + this.children = children; + this.origin_url = origin_url; + this.active = active; + this.username = username; + this.password = password; + this.url = url; + } + + puppeteer_connect = async () => { + this.browser_context = await browser.createBrowserContext(); + + const page = await this.browser_context.newPage(); + + this.page_context = page; + + await this.restoreContext(); + }; + + listen_events = async () => { + if (this.page_context) return; + + await this.puppeteer_connect(); + + await this.action(); + + await this.saveContext(); + }; + + async saveContext() { + if (!this.browser_context || !this.page_context) return; + + try { + const cookies = await this.browser_context.cookies(); + const localStorageData = await this.page_context.evaluate(() => + JSON.stringify(localStorage) + ); + + const contextData = { + cookies, + localStorage: localStorageData, + }; + + const dirPath = path.join(CONSTANTS.PROFILE_PATH); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`📂 Save at folder: ${dirPath}`); + } + + fs.writeFileSync( + path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"), + JSON.stringify(contextData, null, 2) + ); + console.log("✅ Context saved!"); + } catch (error) { + console.log("Save Context: ", error.message); } + } - setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) { - this.created_at = created_at; - this.updated_at = updated_at; - this.children = children; - this.origin_url = origin_url; - this.active = active; - this.username = username; - this.password = password; - this.url = url; - } + async restoreContext() { + if (!this.browser_context || !this.page_context) return; - puppeteer_connect = async () => { - this.browser_context = await browser.createBrowserContext(); + const filePath = getPathProfile(this.origin_url); - const page = await this.browser_context.newPage(); + if (!fs.existsSync(filePath)) return; - this.page_context = page; + const contextData = JSON.parse(fs.readFileSync(filePath, "utf8")); - await this.restoreContext(); - }; + // Restore Cookies + await this.page_context.setCookie(...contextData.cookies); - listen_events = async () => { - if (this.page_context) return; + console.log("🔄 Context restored!"); + } - await this.puppeteer_connect(); + async onCloseLogin() {} - await this.action(); + async isTimeToLogin() { + const earlyLoginTime = findEarlyLoginTime(this); - await this.saveContext(); - }; - - async saveContext() { - if (!this.browser_context || !this.page_context) return; - - try { - const cookies = await this.browser_context.cookies(); - const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage)); - - const contextData = { - cookies, - localStorage: localStorageData, - }; - - const dirPath = path.join(CONSTANTS.PROFILE_PATH); - - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - console.log(`📂 Save at folder: ${dirPath}`); - } - - fs.writeFileSync(path.join(dirPath, sanitizeFileName(this.origin_url) + '.json'), JSON.stringify(contextData, null, 2)); - console.log('✅ Context saved!'); - } catch (error) { - console.log('Save Context: ', error.message); - } - } - - async restoreContext() { - if (!this.browser_context || !this.page_context) return; - - const filePath = getPathProfile(this.origin_url); - - if (!fs.existsSync(filePath)) return; - - const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8')); - - // Restore Cookies - await this.page_context.setCookie(...contextData.cookies); - - console.log('🔄 Context restored!'); - } + return earlyLoginTime && isTimeReached(earlyLoginTime); + } } diff --git a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js index a952f52..dc79ea4 100644 --- a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js +++ b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js @@ -85,6 +85,8 @@ export class LangtonsApiBid extends ApiBid { `➡ [${this.id}] Closing main page context: ${this.page_context}` ); await safeClosePage(this); + + await this.onCloseLogin(this); } console.log(`🔑 [${this.id}] Starting login process...`); diff --git a/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js index 27eda64..9cf982e 100644 --- a/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js +++ b/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js @@ -79,9 +79,21 @@ export class LangtonsProductBid extends ProductBid { timeout / 1000 }s` ); - this.page_context.off("response", onResponse); // Gỡ bỏ listener khi timeout + this.page_context?.off("response", onResponse); // Gỡ bỏ listener khi timeout - await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page + try { + if (!this.page_context.isClosed()) { + await this.page_context.reload({ waitUntil: "networkidle0" }); + console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`); + } else { + console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`); + } + } catch (error) { + console.error( + `❌ [${this.id}] Error reloading page:`, + error?.message + ); + } console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`); resolve(null); @@ -161,8 +173,8 @@ export class LangtonsProductBid extends ProductBid { lot_id: result?.lotId || null, reserve_price: result.lotData?.minimumBid || null, current_price: result.lotData?.currentMaxBid || null, - // close_time: close_time && !this.close_time ? String(close_time) : null, close_time: close_time ? String(close_time) : null, + // close_time: close_time && !this.close_time ? String(close_time) : null, name, }, // [], @@ -441,7 +453,11 @@ export class LangtonsProductBid extends ProductBid { console.log( `⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}` ); - await this.page_context.reload({ waitUntil: "networkidle0" }); + + if (!this.page_context.isClosed()) { + await this.page_context.reload({ waitUntil: "networkidle0" }); + } + console.log(`🔁 [${this.id}] Reload page in gotoLink`); return; } diff --git a/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js b/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js index 023c8aa..3796583 100644 --- a/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js +++ b/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js @@ -248,7 +248,7 @@ export class LawsonsProductBid extends ProductBid { `===============Start call to submit [${this.id}] ================` ); - await delay(2000); + await delay(200); // Nếu chưa bid, thực hiện đặt giá console.log( @@ -368,7 +368,10 @@ export class LawsonsProductBid extends ProductBid { console.log(`✅ [${this.id}] No bid needed. Conditions not met.`); } - if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) { + if ( + new Date(this.updated_at).getTime() > Date.now() - 120 * 1000 && + !this.page_context.isClosed() + ) { await this.page_context.reload({ waitUntil: "networkidle0" }); } } catch (error) { diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index c1f0118..21512fe 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -1,108 +1,147 @@ -import CONSTANTS from './constants.js'; -import fs from 'fs'; -import path from 'path'; -import { updateStatusWork } from './apis/bid.js'; +import CONSTANTS from "./constants.js"; +import fs from "fs"; +import path from "path"; +import { updateStatusWork } from "./apis/bid.js"; export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value)); -export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => { - if (!page || page.isClosed()) return; +export const takeSnapshot = async ( + page, + item, + imageName, + type = CONSTANTS.TYPE_IMAGE.ERRORS +) => { + if (!page || page.isClosed()) return; - try { - const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id - const typeDir = path.join(baseDir, type); // Thư mục con theo type + try { + const baseDir = path.join( + CONSTANTS.ERROR_IMAGES_PATH, + item.type, + String(item.id) + ); // Thư mục theo lot_id + const typeDir = path.join(baseDir, type); // Thư mục con theo type - // Tạo tên file, nếu type === 'work' thì không có timestamp - const fileName = type === CONSTANTS.TYPE_IMAGE.WORK ? `${imageName}.png` : `${imageName}_${new Date().toISOString().replace(/[:.]/g, '-')}.png`; + // Tạo tên file, nếu type === 'work' thì không có timestamp + const fileName = + type === CONSTANTS.TYPE_IMAGE.WORK + ? `${imageName}.png` + : `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`; - const filePath = path.join(typeDir, fileName); + const filePath = path.join(typeDir, fileName); - // Kiểm tra và tạo thư mục nếu chưa tồn tại - if (!fs.existsSync(typeDir)) { - fs.mkdirSync(typeDir, { recursive: true }); - console.log(`📂 Save at folder: ${typeDir}`); - } - - // await page.waitForSelector('body', { visible: true, timeout: 5000 }); - // Kiểm tra có thể điều hướng trang không - const isPageResponsive = await page.evaluate(() => document.readyState === 'complete'); - if (!isPageResponsive) { - console.log('🚫 Page is unresponsive, skipping snapshot.'); - return; - } - - // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua - await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => { - console.log('⚠️ Body selector not found, skipping snapshot.'); - return; - }); - - // Chụp ảnh màn hình và lưu vào filePath - await page.screenshot({ path: filePath }); - - console.log(`📸 Image saved at: ${filePath}`); - - // Nếu type === 'work', gửi ảnh lên API - if (type === CONSTANTS.TYPE_IMAGE.WORK) { - await updateStatusWork(item, filePath); - } - } catch (error) { - console.log('Error when snapshot: ' + error.message); + // Kiểm tra và tạo thư mục nếu chưa tồn tại + if (!fs.existsSync(typeDir)) { + fs.mkdirSync(typeDir, { recursive: true }); + console.log(`📂 Save at folder: ${typeDir}`); } + + // await page.waitForSelector('body', { visible: true, timeout: 5000 }); + // Kiểm tra có thể điều hướng trang không + const isPageResponsive = await page.evaluate( + () => document.readyState === "complete" + ); + if (!isPageResponsive) { + console.log("🚫 Page is unresponsive, skipping snapshot."); + return; + } + + // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua + await page + .waitForSelector("body", { visible: true, timeout: 15000 }) + .catch(() => { + console.log("⚠️ Body selector not found, skipping snapshot."); + return; + }); + + // Chụp ảnh màn hình và lưu vào filePath + await page.screenshot({ path: filePath }); + + console.log(`📸 Image saved at: ${filePath}`); + + // Nếu type === 'work', gửi ảnh lên API + if (type === CONSTANTS.TYPE_IMAGE.WORK) { + await updateStatusWork(item, filePath); + } + } catch (error) { + console.log("Error when snapshot: " + error.message); + } }; export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -export async function safeClosePage(item) { - try { - const page = item.page_context; +export const safeClosePageReal = async (page) => { + if (!page) return; - if (!page?.isClosed() && page?.close) { - await page.close(); - } - - item.page_context = undefined; - if (item?.page_context) { - item.page_context = undefined; - } - } catch (error) { - console.log("Can't close item: " + item.id); + try { + if (page.isClosed()) { + console.log(`✅ Page already closed: ${page.url()}`); + return; } + + page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close + await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn + console.log(`✅ Successfully closed page: ${page.url()}`); + } catch (err) { + console.warn( + `⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}` + ); + } +}; + +export async function safeClosePage(item) { + try { + const page = item.page_context; + + if (!page?.isClosed() && page?.close) { + await safeClosePageReal(page); + // await page.close(); + } + + item.page_context = undefined; + if (item?.page_context) { + item.page_context = undefined; + } + } catch (error) { + console.log("Can't close item: " + item.id); + } } export function isTimeReached(targetTime) { - if (!targetTime) return false; + if (!targetTime) return false; - const targetDate = new Date(targetTime); - const now = new Date(); + const targetDate = new Date(targetTime); + const now = new Date(); - return now >= targetDate; + return now >= targetDate; } export function extractNumber(str) { - const match = str.match(/\d+(\.\d+)?/); - return match ? parseFloat(match[0]) : null; + const match = str.match(/\d+(\.\d+)?/); + return match ? parseFloat(match[0]) : null; } export const sanitizeFileName = (url) => { - return url.replace(/[:\/]/g, '_'); + return url.replace(/[:\/]/g, "_"); }; export const getPathProfile = (origin_url) => { - return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json'); + return path.join( + CONSTANTS.PROFILE_PATH, + sanitizeFileName(origin_url) + ".json" + ); }; export function removeFalsyValues(obj, excludeKeys = []) { - return Object.entries(obj).reduce((acc, [key, value]) => { - if (value || excludeKeys.includes(key)) { - acc[key] = value; - } - return acc; - }, {}); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (value || excludeKeys.includes(key)) { + acc[key] = value; + } + return acc; + }, {}); } export const enableAutoBidMessage = (data) => { - return ` + return ` ⭉ Activate Auto Bid
📌 Product: ${data.name}
🔗 Link: Click here
@@ -112,69 +151,110 @@ export const enableAutoBidMessage = (data) => { }; export function convertAETtoUTC(dateString) { - // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript) - const monthMap = { - Jan: 0, - Feb: 1, - Mar: 2, - Apr: 3, - May: 4, - Jun: 5, - Jul: 6, - Aug: 7, - Sep: 8, - Oct: 9, - Nov: 10, - Dec: 11, - }; + // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript) + const monthMap = { + Jan: 0, + Feb: 1, + Mar: 2, + Apr: 3, + May: 4, + Jun: 5, + Jul: 6, + Aug: 7, + Sep: 8, + Oct: 9, + Nov: 10, + Dec: 11, + }; - // Tách chuỗi đầu vào - const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/); - if (!parts) { - throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'"); - } + // Tách chuỗi đầu vào + const parts = dateString.match( + /(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/ + ); + if (!parts) { + throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'"); + } - const [, , day, month, year, hour, period] = parts; + const [, , day, month, year, hour, period] = parts; - // Chuyển đổi giờ sang định dạng 24h - let hours = parseInt(hour, 10); - if (period === 'PM' && hours !== 12) hours += 12; - if (period === 'AM' && hours === 12) hours = 0; + // Chuyển đổi giờ sang định dạng 24h + let hours = parseInt(hour, 10); + if (period === "PM" && hours !== 12) hours += 12; + if (period === "AM" && hours === 12) hours = 0; - // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ) - const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0)); + // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ) + const date = new Date( + Date.UTC( + parseInt(year, 10), + monthMap[month], + parseInt(day, 10), + hours, + 0, + 0 + ) + ); - // Hàm kiểm tra DST cho AET - function isDST(date) { - const year = date.getUTCFullYear(); - const month = date.getUTCMonth(); - const day = date.getUTCDate(); + // Hàm kiểm tra DST cho AET + function isDST(date) { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const day = date.getUTCDate(); - // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT) - const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10 - dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên - const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10 + // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT) + const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10 + dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên + const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10 - // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST) - const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4 - dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên - const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11 + // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST) + const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4 + dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên + const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11 - const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10) - return currentTime >= dstStartTime && currentTime < dstEndTime; - } + const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10) + return currentTime >= dstStartTime && currentTime < dstEndTime; + } - // Xác định offset dựa trên DST - const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không + // Xác định offset dựa trên DST + const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không - // Điều chỉnh thời gian về UTC - const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000); + // Điều chỉnh thời gian về UTC + const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000); - // Trả về chuỗi UTC - return utcDate.toUTCString(); + // Trả về chuỗi UTC + return utcDate.toUTCString(); } export function extractPriceNumber(priceString) { - const cleaned = priceString.replace(/[^\d.]/g, ''); - return parseFloat(cleaned); + const cleaned = priceString.replace(/[^\d.]/g, ""); + return parseFloat(cleaned); +} + +export function findEarlyLoginTime(webBid) { + const now = new Date(); + + // Bước 1: Lọc ra những bid có close_time hợp lệ + const validChildren = webBid.children.filter((child) => child.close_time); + + if (validChildren.length === 0) return null; + + // Bước 2: Tìm bid có close_time gần hiện tại nhất + const closestBid = validChildren.reduce((closest, current) => { + const closestDiff = Math.abs( + new Date(closest.close_time).getTime() - now.getTime() + ); + const currentDiff = Math.abs( + new Date(current.close_time).getTime() - now.getTime() + ); + return currentDiff < closestDiff ? current : closest; + }); + + if (!closestBid.close_time) return null; + + // Bước 3: Tính toán thời gian login sớm + const closeTime = new Date(closestBid.close_time); + closeTime.setSeconds( + closeTime.getSeconds() - (webBid.early_login_seconds || 0) + ); + + return closeTime.toISOString(); }