Deploy to Production #20
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -38,6 +38,7 @@
 | 
			
		|||
        "tailwind-merge": "^3.0.1",
 | 
			
		||||
        "tailwindcss": "^4.0.6",
 | 
			
		||||
        "uuid": "^11.0.5",
 | 
			
		||||
        "yet-another-react-lightbox": "^3.22.0",
 | 
			
		||||
        "zod": "^3.24.1",
 | 
			
		||||
        "zustand": "^5.0.3"
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			@ -1422,7 +1423,7 @@
 | 
			
		|||
      "version": "19.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "devOptional": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@types/react": "^19.0.0"
 | 
			
		||||
| 
						 | 
				
			
			@ -7541,6 +7542,29 @@
 | 
			
		|||
      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
 | 
			
		||||
      "license": "ISC"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/yet-another-react-lightbox": {
 | 
			
		||||
      "version": "3.22.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.22.0.tgz",
 | 
			
		||||
      "integrity": "sha512-yaXmzUraH/Ftsp7eG/E2leQgXhtrG8c1t+jImlSjC2XtZ7XkvjIV2vP/1kl5kxmsBHjck/98W/9Xxempry+2QQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=14"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "@types/react": "^16 || ^17 || ^18 || ^19",
 | 
			
		||||
        "@types/react-dom": "^16 || ^17 || ^18 || ^19",
 | 
			
		||||
        "react": "^16.8.0 || ^17 || ^18 || ^19",
 | 
			
		||||
        "react-dom": "^16.8.0 || ^17 || ^18 || ^19"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependenciesMeta": {
 | 
			
		||||
        "@types/react": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        },
 | 
			
		||||
        "@types/react-dom": {
 | 
			
		||||
          "optional": true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/zod": {
 | 
			
		||||
      "version": "3.24.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@
 | 
			
		|||
    "tailwind-merge": "^3.0.1",
 | 
			
		||||
    "tailwindcss": "^4.0.6",
 | 
			
		||||
    "uuid": "^11.0.5",
 | 
			
		||||
    "yet-another-react-lightbox": "^3.22.0",
 | 
			
		||||
    "zod": "^3.24.1",
 | 
			
		||||
    "zustand": "^5.0.3"
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
			
		||||
import { generateNestParams, handleError, handleSuccess } from '.';
 | 
			
		||||
import axios from '../lib/axios';
 | 
			
		||||
import { IAdmin } from '../system/type';
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +52,8 @@ export const grantNewPasswordAdmin = async (admin: Partial<IAdmin>) => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const createAdmin = async (admin: Omit<IAdmin, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
 | 
			
		||||
    const newData = removeFalsyValues(admin);
 | 
			
		||||
    const {permissions , ...newData} = removeFalsyValues(admin);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,6 +115,6 @@ export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string
 | 
			
		|||
 | 
			
		||||
        return data;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
        console.log('%csrc/apis/bid.ts:118 error', 'color: #007acc;', error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,3 +34,18 @@ export const shutdownTool = async () => {
 | 
			
		|||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const getStatusTool = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        const { data } = await axios({
 | 
			
		||||
            url: `${BASE_URL}/status-tool`,
 | 
			
		||||
            withCredentials: true,
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return data;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        handleError(error);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ export const handleError = (error: unknown) => {
 | 
			
		|||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    const response = (error as AxiosError).response as Record<string, any>;
 | 
			
		||||
 | 
			
		||||
    const data = response.data;
 | 
			
		||||
    const data = response?.data;
 | 
			
		||||
 | 
			
		||||
    if (response.status === HttpStatusCode.Forbidden) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,27 +1,29 @@
 | 
			
		|||
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<string, string | number>) => {
 | 
			
		||||
  return await axios({
 | 
			
		||||
    url: BASE_URL,
 | 
			
		||||
    params: generateNestParams(params),
 | 
			
		||||
    withCredentials: true,
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
    method: "GET",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
 | 
			
		||||
export const createWebBid = async (
 | 
			
		||||
  bid: Omit<IWebBid, "id" | "created_at" | "updated_at" | "is_system_account">
 | 
			
		||||
) => {
 | 
			
		||||
  const newData = removeFalsyValues(bid);
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
      url: BASE_URL,
 | 
			
		||||
      withCredentials: true,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      data: newData,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,14 +36,30 @@ export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'upd
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const updateWebBid = async (bid: Partial<IWebBid>) => {
 | 
			
		||||
    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 },
 | 
			
		||||
      method: "PUT",
 | 
			
		||||
      data: {
 | 
			
		||||
        url,
 | 
			
		||||
        password,
 | 
			
		||||
        username,
 | 
			
		||||
        origin_url,
 | 
			
		||||
        active,
 | 
			
		||||
        arrival_offset_seconds,
 | 
			
		||||
        // early_login_seconds
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    handleSuccess(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +75,7 @@ export const deleteWebBid = async (web: IWebBid) => {
 | 
			
		|||
    const { data } = await axios({
 | 
			
		||||
      url: `${BASE_URL}/` + web.id,
 | 
			
		||||
      withCredentials: true,
 | 
			
		||||
            method: 'DELETE',
 | 
			
		||||
      method: "DELETE",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    handleSuccess(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +95,7 @@ export const deletesWebBid = async (web: IWebBid[]) => {
 | 
			
		|||
    const { data } = await axios({
 | 
			
		||||
      url: `${BASE_URL}/deletes`,
 | 
			
		||||
      withCredentials: true,
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      data: {
 | 
			
		||||
        ids,
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,14 +127,14 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
 | 
			
		|||
            centered
 | 
			
		||||
        >
 | 
			
		||||
            <form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
 | 
			
		||||
                <TextInput readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
 | 
			
		||||
                <TextInput size="sm" label="Email" {...form.getInputProps('email')} />
 | 
			
		||||
                <TextInput className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
 | 
			
		||||
                <TextInput withAsterisk readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
 | 
			
		||||
                <TextInput withAsterisk size="sm" label="Email" {...form.getInputProps('email')} />
 | 
			
		||||
                <TextInput withAsterisk className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
 | 
			
		||||
 | 
			
		||||
                {!data && (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <PasswordInput size="sm" label="Password" {...form.getInputProps('password')} />
 | 
			
		||||
                        <PasswordInput size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
 | 
			
		||||
                        <PasswordInput withAsterisk size="sm" label="Password" {...form.getInputProps('password')} />
 | 
			
		||||
                        <PasswordInput withAsterisk size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,8 +87,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
 | 
			
		|||
            centered
 | 
			
		||||
        >
 | 
			
		||||
            <form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
 | 
			
		||||
                <PasswordInput className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
 | 
			
		||||
                <PasswordInput className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
 | 
			
		||||
                <PasswordInput withAsterisk className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
 | 
			
		||||
                <PasswordInput withAsterisk className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
 | 
			
		||||
 | 
			
		||||
                <Button className="col-span-2" type="submit" fullWidth size="sm" mt="md">
 | 
			
		||||
                    {'Grant'}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
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';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,8 +17,8 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
 | 
			
		|||
    const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const rows = useMemo(() => {
 | 
			
		||||
        return histories.map((element) => (
 | 
			
		||||
            <Table.Tr key={element.LotId}>
 | 
			
		||||
        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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
/* 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 { formatTime } from '../../utils';
 | 
			
		||||
 | 
			
		||||
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
 | 
			
		||||
    data: IBid | null;
 | 
			
		||||
    onUpdated?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ShowHistoriesBidPicklesApiModal({ 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['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]);
 | 
			
		||||
 | 
			
		||||
    const handleCallApi = useCallback(async () => {
 | 
			
		||||
        if (!data?.lot_id) {
 | 
			
		||||
            setHistories([]);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setLoading(true);
 | 
			
		||||
        const response = await getDetailBidHistories(data?.lot_id);
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
 | 
			
		||||
        if (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>Bidder name</Table.Th>
 | 
			
		||||
                        <Table.Th>Actual bid</Table.Th>
 | 
			
		||||
                        <Table.Th>Time</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>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH
 | 
			
		|||
        <Table.Tr key={element.id}>
 | 
			
		||||
            <Table.Td>{element.id}</Table.Td>
 | 
			
		||||
            <Table.Td>{element.price}</Table.Td>
 | 
			
		||||
            <Table.Td>{formatTime(element.created_at, 'DD/MM/YYYY HH:MM')}</Table.Td>
 | 
			
		||||
            <Table.Td>{formatTime(new Date(element.created_at).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
 | 
			
		||||
        </Table.Tr>
 | 
			
		||||
    ));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,14 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
			
		||||
import { Image, Modal, ModalProps, ScrollArea } from '@mantine/core';
 | 
			
		||||
 | 
			
		||||
export default function ShowImageModal({ src, fallbackSrc, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
 | 
			
		||||
import { ModalProps } from '@mantine/core';
 | 
			
		||||
import Lightbox from "yet-another-react-lightbox";
 | 
			
		||||
import "yet-another-react-lightbox/plugins/captions.css";
 | 
			
		||||
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
 | 
			
		||||
import "yet-another-react-lightbox/plugins/thumbnails.css";
 | 
			
		||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
 | 
			
		||||
import "yet-another-react-lightbox/styles.css";
 | 
			
		||||
export default function ShowImageModal({ src, fallbackSrc,opened, onClose, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
 | 
			
		||||
    return (
 | 
			
		||||
        <Modal
 | 
			
		||||
            classNames={{
 | 
			
		||||
                header: '!flex !item-center !justify-center w-full',
 | 
			
		||||
            }}
 | 
			
		||||
            {...props}
 | 
			
		||||
            size={'xl'}
 | 
			
		||||
            title={<span className="text-xl font-bold">Image</span>}
 | 
			
		||||
            centered
 | 
			
		||||
            scrollAreaComponent={ScrollArea.Autosize}
 | 
			
		||||
        >
 | 
			
		||||
            <Image src={src} fallbackSrc={fallbackSrc} />
 | 
			
		||||
        </Modal>
 | 
			
		||||
        <Lightbox {...props} open={opened} close={onClose}  slides={[{ src: src || fallbackSrc }]} 
 | 
			
		||||
            plugins={[Fullscreen, Zoom]}/>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,21 @@
 | 
			
		|||
import { Box, Button, Image, Text } from '@mantine/core';
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { Socket } from 'socket.io-client';
 | 
			
		||||
import { getImagesWorking } from '../../apis/bid';
 | 
			
		||||
import { IBid, IWebBid } from '../../system/type';
 | 
			
		||||
import ShowImageModal from './show-image-modal';
 | 
			
		||||
 | 
			
		||||
import { Badge, Box, Button, Image, Text } from "@mantine/core";
 | 
			
		||||
import { useDisclosure } from "@mantine/hooks";
 | 
			
		||||
import moment from "moment";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Socket } from "socket.io-client";
 | 
			
		||||
import { getImagesWorking } from "../../apis/bid";
 | 
			
		||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
 | 
			
		||||
import { IBid, IWebBid } from "../../system/type";
 | 
			
		||||
import { cn, stringToColor } from "../../utils";
 | 
			
		||||
import ShowImageModal from "./show-image-modal";
 | 
			
		||||
export interface IWorkingPageProps {
 | 
			
		||||
  data: (IBid | IWebBid) & { type: string };
 | 
			
		||||
  socket: Socket;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
			
		||||
    const fallbackSrc = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s';
 | 
			
		||||
  const fallbackSrc =
 | 
			
		||||
    "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s";
 | 
			
		||||
 | 
			
		||||
  const [opened, { open, close }] = useDisclosure(false);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,34 +23,85 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
			
		|||
 | 
			
		||||
  const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
 | 
			
		||||
 | 
			
		||||
  const [payloadLoginStatus, setPayloadLoginStatus] = useState<{
 | 
			
		||||
    data: IWebBid;
 | 
			
		||||
    login_status: boolean;
 | 
			
		||||
  } | null>(null);
 | 
			
		||||
 | 
			
		||||
  const { statusTool } = useStatusToolStore();
 | 
			
		||||
 | 
			
		||||
  function isIBid(obj: IBid | IWebBid): obj is IBid {
 | 
			
		||||
        return 'name' in obj;
 | 
			
		||||
    return "name" in obj;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    const renderUrl = ({ type, id }: (IBid | IWebBid) & { type: string }, name: string) => {
 | 
			
		||||
        return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`;
 | 
			
		||||
  const renderUrl = (
 | 
			
		||||
    { type, id }: (IBid | IWebBid) & { type: string },
 | 
			
		||||
    name: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type
 | 
			
		||||
      .replace("_", "-")
 | 
			
		||||
      .toLowerCase()}/${id}/${name}`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const extractTime = (filename: string) => {
 | 
			
		||||
        return Number(filename.split('-')[0]) || 0;
 | 
			
		||||
    return Number(filename.split("-")[0]) || 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const statusLabel = () => {
 | 
			
		||||
    if (
 | 
			
		||||
      statusTool &&
 | 
			
		||||
      statusTool === "online" &&
 | 
			
		||||
      payloadLoginStatus?.login_status
 | 
			
		||||
    ) {
 | 
			
		||||
      return "logined";
 | 
			
		||||
    }
 | 
			
		||||
    return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
        const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => {
 | 
			
		||||
    const updateImage = ({
 | 
			
		||||
      type,
 | 
			
		||||
      id,
 | 
			
		||||
      filename,
 | 
			
		||||
    }: {
 | 
			
		||||
      type: string;
 | 
			
		||||
      filename: string;
 | 
			
		||||
      id: IBid["id"];
 | 
			
		||||
    }) => {
 | 
			
		||||
      if (type == data.type && id == data.id) {
 | 
			
		||||
        setLastUpdate(new Date(extractTime(filename)));
 | 
			
		||||
        setImageSrc(renderUrl(data, filename));
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
        socket.on('working', updateImage);
 | 
			
		||||
    socket.on("working", updateImage);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
            socket.off('working', updateImage);
 | 
			
		||||
      socket.off("working", updateImage);
 | 
			
		||||
    };
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [socket, data.id, data.type]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onLoginStatus = (data: { data: IWebBid; login_status: boolean }) => {
 | 
			
		||||
      setPayloadLoginStatus(data);
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        "%csrc/components/dashboard/working-page.tsx:60 data",
 | 
			
		||||
        "color: #007acc;",
 | 
			
		||||
        data
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const origin_url = isIBid(data) ? data.web_bid.origin_url : data.origin_url;
 | 
			
		||||
 | 
			
		||||
    socket.on(`login-status.${origin_url}`, onLoginStatus);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      socket.off(`login-status.${origin_url}`, onLoginStatus);
 | 
			
		||||
    };
 | 
			
		||||
  }, [data, socket]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    (async () => {
 | 
			
		||||
      const result = await getImagesWorking(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -65,40 +118,79 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
			
		|||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
            <Box className="rounded-md overflow-hidden relative shadow-lg">
 | 
			
		||||
      <Box
 | 
			
		||||
        className={cn("rounded-md overflow-hidden relative shadow-lg", {
 | 
			
		||||
          ["border border-green-800"]: payloadLoginStatus?.login_status,
 | 
			
		||||
          ["border border-red-800"]: !payloadLoginStatus?.login_status,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <Image
 | 
			
		||||
          radius="md"
 | 
			
		||||
          h={300}
 | 
			
		||||
          style={{
 | 
			
		||||
                        objectFit: 'cover',
 | 
			
		||||
            objectFit: "cover",
 | 
			
		||||
          }}
 | 
			
		||||
          fallbackSrc={fallbackSrc}
 | 
			
		||||
          src={imageSrc}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
 | 
			
		||||
                    <Text className="text-lg tracking-wide text-center font-bold">{isIBid(data) ? data.name : 'Tracking page'}</Text>
 | 
			
		||||
                    {isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
 | 
			
		||||
                    {isIBid(data) && <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>}
 | 
			
		||||
                    <Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text>
 | 
			
		||||
          <Text className="text-lg tracking-wide text-center font-bold">
 | 
			
		||||
            {isIBid(data) ? data.name : "Tracking page"}
 | 
			
		||||
          </Text>
 | 
			
		||||
          {isIBid(data) && (
 | 
			
		||||
            <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>
 | 
			
		||||
          )}
 | 
			
		||||
          {isIBid(data) && (
 | 
			
		||||
            <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
 | 
			
		||||
          )}
 | 
			
		||||
          <Text className="text-sm italic opacity-80">
 | 
			
		||||
            {moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
 | 
			
		||||
          </Text>
 | 
			
		||||
          <Box className="flex items-center gap-4">
 | 
			
		||||
                        <Button size="xs" color="green" onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
 | 
			
		||||
            <Button
 | 
			
		||||
              size="xs"
 | 
			
		||||
              color="green"
 | 
			
		||||
              onClick={open}
 | 
			
		||||
              className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
 | 
			
		||||
            >
 | 
			
		||||
              Show
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              component="a"
 | 
			
		||||
              size="xs"
 | 
			
		||||
                            href={data.url || '/'}
 | 
			
		||||
              href={data.url || "/"}
 | 
			
		||||
              className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
 | 
			
		||||
            >
 | 
			
		||||
              Link
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
 | 
			
		||||
        <Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
 | 
			
		||||
          <Badge
 | 
			
		||||
            color={payloadLoginStatus?.login_status ? "green" : "red"}
 | 
			
		||||
            size="xs"
 | 
			
		||||
          >
 | 
			
		||||
            {statusLabel()}
 | 
			
		||||
          </Badge>
 | 
			
		||||
 | 
			
		||||
          <Badge
 | 
			
		||||
            color={stringToColor(isIBid(data) ? data.web_bid.origin_url : data.origin_url)}
 | 
			
		||||
            size="xs"
 | 
			
		||||
          >
 | 
			
		||||
            {isIBid(data) ? data.web_bid.origin_url : data.origin_url}
 | 
			
		||||
          </Badge>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
            <ShowImageModal src={imageSrc || fallbackSrc} fallbackSrc={fallbackSrc} opened={opened} onClose={close} />
 | 
			
		||||
      <ShowImageModal
 | 
			
		||||
        src={imageSrc || fallbackSrc}
 | 
			
		||||
        fallbackSrc={fallbackSrc}
 | 
			
		||||
        opened={opened}
 | 
			
		||||
        onClose={close}
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,44 @@
 | 
			
		|||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
export default function WebBidModal({
 | 
			
		||||
  data,
 | 
			
		||||
  onUpdated,
 | 
			
		||||
  ...props
 | 
			
		||||
}: IWebBidModelProps) {
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    validate: zodResolver(z.object(schema)),
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -31,10 +52,15 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
 | 
			
		|||
  const handleSubmit = async (values: typeof form.values) => {
 | 
			
		||||
    if (data) {
 | 
			
		||||
      setConfirm({
 | 
			
		||||
                title: 'Update ?',
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,15 +73,20 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
 | 
			
		|||
          }
 | 
			
		||||
        },
 | 
			
		||||
        okButton: {
 | 
			
		||||
                    color: 'blue',
 | 
			
		||||
                    value: 'Update',
 | 
			
		||||
          color: "blue",
 | 
			
		||||
          value: "Update",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
            const { url, origin_url } = values;
 | 
			
		||||
      const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
 | 
			
		||||
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
            const result = await createWebBid({ url, origin_url } as IWebBid);
 | 
			
		||||
      const result = await createWebBid({
 | 
			
		||||
        url,
 | 
			
		||||
        origin_url,
 | 
			
		||||
        arrival_offset_seconds,
 | 
			
		||||
        early_login_seconds
 | 
			
		||||
      } as IWebBid);
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
 | 
			
		||||
      if (!result) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +118,7 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (form.values?.url) {
 | 
			
		||||
            form.setFieldValue('origin_url', extractDomain(form.values.url));
 | 
			
		||||
      form.setFieldValue("origin_url", extractDomain(form.values.url));
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [form.values]);
 | 
			
		||||
| 
						 | 
				
			
			@ -96,23 +127,70 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
 | 
			
		|||
    <Modal
 | 
			
		||||
      className="relative"
 | 
			
		||||
      classNames={{
 | 
			
		||||
                header: '!flex !item-center !justify-center w-full',
 | 
			
		||||
        header: "!flex !item-center !justify-center w-full",
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
            size={'xl'}
 | 
			
		||||
      size={"xl"}
 | 
			
		||||
      title={<span className="text-xl font-bold">Web</span>}
 | 
			
		||||
      centered
 | 
			
		||||
    >
 | 
			
		||||
            <form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
 | 
			
		||||
                <TextInput className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
 | 
			
		||||
                <TextInput className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
 | 
			
		||||
      <form
 | 
			
		||||
        onSubmit={form.onSubmit(handleSubmit)}
 | 
			
		||||
        className="grid grid-cols-2 gap-2.5"
 | 
			
		||||
      >
 | 
			
		||||
        <TextInput
 | 
			
		||||
          withAsterisk
 | 
			
		||||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label="Domain"
 | 
			
		||||
          {...form.getInputProps("origin_url")}
 | 
			
		||||
        />
 | 
			
		||||
        <TextInput
 | 
			
		||||
          withAsterisk
 | 
			
		||||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label="Tracking url"
 | 
			
		||||
          {...form.getInputProps("url")}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
                <Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
 | 
			
		||||
                    {data ? 'Update' : 'Create'}
 | 
			
		||||
        <NumberInput
 | 
			
		||||
          description="Note: that only integer minutes are accepted."
 | 
			
		||||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label={`Arrival offset seconds (${
 | 
			
		||||
            form.getValues()["arrival_offset_seconds"] / 60
 | 
			
		||||
          } minutes)`}
 | 
			
		||||
          placeholder="msg: 300"
 | 
			
		||||
          {...form.getInputProps("arrival_offset_seconds")}
 | 
			
		||||
        />
 | 
			
		||||
        {/* <NumberInput
 | 
			
		||||
          description="Note: that only integer minutes are accepted."
 | 
			
		||||
          className="col-span-2"
 | 
			
		||||
          size="sm"
 | 
			
		||||
          label={`Early login seconds (${
 | 
			
		||||
            form.getValues()["early_login_seconds"] / 60
 | 
			
		||||
          } minutes)`}
 | 
			
		||||
          placeholder="msg: 600"
 | 
			
		||||
          {...form.getInputProps("early_login_seconds")}
 | 
			
		||||
        /> */}
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
          disabled={_.isEqual(form.getValues(), prevData.current)}
 | 
			
		||||
          className="col-span-2"
 | 
			
		||||
          type="submit"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          size="sm"
 | 
			
		||||
          mt="md"
 | 
			
		||||
        >
 | 
			
		||||
          {data ? "Update" : "Create"}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </form>
 | 
			
		||||
 | 
			
		||||
            <LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
 | 
			
		||||
      <LoadingOverlay
 | 
			
		||||
        visible={loading}
 | 
			
		||||
        zIndex={1000}
 | 
			
		||||
        overlayProps={{ blur: 2 }}
 | 
			
		||||
      />
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
 const constants = {
 | 
			
		||||
    grays: 'https://www.grays.com',
 | 
			
		||||
    pickles:'https://www.pickles.com.au'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const haveHistories = [constants.grays, constants.pickles]
 | 
			
		||||
 | 
			
		||||
export default constants
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
import { create } from "zustand";
 | 
			
		||||
 | 
			
		||||
type TStatusToolState = {
 | 
			
		||||
  statusTool: string | boolean;
 | 
			
		||||
  setStatusTool: (value: TStatusToolState["statusTool"]) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useStatusToolStore = create<TStatusToolState>((set) => ({
 | 
			
		||||
  statusTool: false,
 | 
			
		||||
  props: {},
 | 
			
		||||
  setStatusTool: (value: TStatusToolState["statusTool"]) =>
 | 
			
		||||
    set({ statusTool: value }),
 | 
			
		||||
}));
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +1,30 @@
 | 
			
		|||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks';
 | 
			
		||||
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
 | 
			
		||||
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
 | 
			
		||||
import Table from '../lib/table/table';
 | 
			
		||||
import { IColumn, TRefTableFn } from '../lib/table/type';
 | 
			
		||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
			
		||||
import { mappingStatusColors } from '../system/constants';
 | 
			
		||||
import { IBid } from '../system/type';
 | 
			
		||||
import { formatTime } from '../utils';
 | 
			
		||||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
 | 
			
		||||
import { useDisclosure } from "@mantine/hooks";
 | 
			
		||||
import {
 | 
			
		||||
  IconAd,
 | 
			
		||||
  IconAdOff,
 | 
			
		||||
  IconEdit,
 | 
			
		||||
  IconHammer,
 | 
			
		||||
  IconHistory,
 | 
			
		||||
  IconMenu,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
} 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 Table from "../lib/table/table";
 | 
			
		||||
import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
			
		||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
			
		||||
import { mappingStatusColors } from "../system/constants";
 | 
			
		||||
import { IBid } from "../system/type";
 | 
			
		||||
import { formatTime } from "../utils";
 | 
			
		||||
import constants, { haveHistories } from "../constant";
 | 
			
		||||
 | 
			
		||||
export default function Bids() {
 | 
			
		||||
  const refTableFn: TRefTableFn<IBid> = useRef({});
 | 
			
		||||
| 
						 | 
				
			
			@ -20,97 +34,109 @@ export default function Bids() {
 | 
			
		|||
  const { setConfirm } = useConfirmStore();
 | 
			
		||||
 | 
			
		||||
  const [openedHistories, historiesModel] = useDisclosure(false);
 | 
			
		||||
    const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
 | 
			
		||||
  const [openedHistoriesGraysApi, historiesGraysApiModel] =
 | 
			
		||||
    useDisclosure(false);
 | 
			
		||||
 | 
			
		||||
  const [openedHistoriesPicklesApi, historiesPicklesApiModel] =
 | 
			
		||||
    useDisclosure(false);
 | 
			
		||||
  const [openedBid, bidModal] = useDisclosure(false);
 | 
			
		||||
 | 
			
		||||
  const columns: IColumn<IBid>[] = [
 | 
			
		||||
    {
 | 
			
		||||
            key: 'id',
 | 
			
		||||
            title: 'ID',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
      key: "id",
 | 
			
		||||
      title: "ID",
 | 
			
		||||
      typeFilter: "number",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'name',
 | 
			
		||||
            title: 'Name',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "name",
 | 
			
		||||
      title: "Name",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'web_bid',
 | 
			
		||||
            title: 'Web',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "web_bid",
 | 
			
		||||
      title: "Web",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
        return <span>{row.web_bid.origin_url}</span>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'lot_id',
 | 
			
		||||
            title: 'Lot ID',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "lot_id",
 | 
			
		||||
      title: "Lot ID",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'model',
 | 
			
		||||
            title: 'Model',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "model",
 | 
			
		||||
      title: "Model",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
            key: 'plus_price',
 | 
			
		||||
            title: 'Plus price',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
      key: "plus_price",
 | 
			
		||||
      title: "Plus price",
 | 
			
		||||
      typeFilter: "number",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'max_price',
 | 
			
		||||
            title: 'Max price',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
      key: "max_price",
 | 
			
		||||
      title: "Max price",
 | 
			
		||||
      typeFilter: "number",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'current_price',
 | 
			
		||||
            title: 'Current price',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
      key: "current_price",
 | 
			
		||||
      title: "Current price",
 | 
			
		||||
      typeFilter: "number",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'reserve_price',
 | 
			
		||||
            title: 'Reserve price',
 | 
			
		||||
            typeFilter: 'number',
 | 
			
		||||
      key: "reserve_price",
 | 
			
		||||
      title: "Reserve price",
 | 
			
		||||
      typeFilter: "number",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'histories',
 | 
			
		||||
            title: 'Current bid',
 | 
			
		||||
            typeFilter: 'none',
 | 
			
		||||
      key: "histories",
 | 
			
		||||
      title: "Current bid",
 | 
			
		||||
      typeFilter: "none",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
                const bidPrice = _.maxBy(row.histories, 'price');
 | 
			
		||||
        const bidPrice = _.maxBy(row.histories, "price");
 | 
			
		||||
 | 
			
		||||
                return <Text>{bidPrice ? bidPrice.price : 'None'}</Text>;
 | 
			
		||||
        return <Text>{bidPrice ? bidPrice.price : "None"}</Text>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'start_bid_time',
 | 
			
		||||
            title: 'Start bid',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "start_bid_time",
 | 
			
		||||
      title: "Start bid",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Tooltip hidden={!row.start_bid_time} label={row.start_bid_time}>
 | 
			
		||||
                        <Text size="sm">{row.start_bid_time ? formatTime(row.start_bid_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
 | 
			
		||||
            <Text size="sm">
 | 
			
		||||
              {row.start_bid_time
 | 
			
		||||
                ? formatTime(row.start_bid_time, "HH:mm:ss DD/MM/YYYY")
 | 
			
		||||
                : "None"}
 | 
			
		||||
            </Text>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'close_time',
 | 
			
		||||
            title: 'Close time',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "close_time",
 | 
			
		||||
      title: "Close time",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Tooltip hidden={!row.close_time} label={row.close_time}>
 | 
			
		||||
                        <Text size="sm">{row.close_time ? formatTime(row.close_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
 | 
			
		||||
            <Text size="sm">
 | 
			
		||||
              {row.close_time
 | 
			
		||||
                ? formatTime(row.close_time, "HH:mm:ss DD/MM/YYYY")
 | 
			
		||||
                : "None"}
 | 
			
		||||
            </Text>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
            key: 'status',
 | 
			
		||||
            title: 'Status',
 | 
			
		||||
            typeFilter: 'text',
 | 
			
		||||
      key: "status",
 | 
			
		||||
      title: "Status",
 | 
			
		||||
      typeFilter: "text",
 | 
			
		||||
      renderRow(row) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Box className="flex items-center justify-center">
 | 
			
		||||
| 
						 | 
				
			
			@ -125,8 +151,8 @@ export default function Bids() {
 | 
			
		|||
 | 
			
		||||
  const handleDelete = (bid: IBid) => {
 | 
			
		||||
    setConfirm({
 | 
			
		||||
            title: 'Delete ?',
 | 
			
		||||
            message: 'This bid will be delete',
 | 
			
		||||
      title: "Delete ?",
 | 
			
		||||
      message: "This bid will be delete",
 | 
			
		||||
      handleOk: async () => {
 | 
			
		||||
        await deleteBid(bid);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -138,11 +164,12 @@ export default function Bids() {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  const handleToggleBid = async (bid: IBid) => {
 | 
			
		||||
        const isEnable = bid.status === 'biding' ? true : bid.status === 'out-bid' ? false : true;
 | 
			
		||||
    const isEnable =
 | 
			
		||||
      bid.status === "biding" ? true : bid.status === "out-bid" ? false : true;
 | 
			
		||||
 | 
			
		||||
    setConfirm({
 | 
			
		||||
            title: (isEnable ? 'Disable ' : 'Enable ') + 'ID: ' + bid.id,
 | 
			
		||||
            message: 'This bid will be ' + (isEnable ? 'disable ' : 'enable '),
 | 
			
		||||
      title: (isEnable ? "Disable " : "Enable ") + "ID: " + bid.id,
 | 
			
		||||
      message: "This bid will be " + (isEnable ? "disable " : "enable "),
 | 
			
		||||
      handleOk: async () => {
 | 
			
		||||
        await toggleBid(bid);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -151,8 +178,8 @@ export default function Bids() {
 | 
			
		|||
        }
 | 
			
		||||
      },
 | 
			
		||||
      okButton: {
 | 
			
		||||
                value: isEnable ? 'Disable ' : 'Enable ',
 | 
			
		||||
                color: isEnable ? 'red' : 'blue',
 | 
			
		||||
        value: isEnable ? "Disable " : "Enable ",
 | 
			
		||||
        color: isEnable ? "red" : "blue",
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -161,29 +188,29 @@ export default function Bids() {
 | 
			
		|||
    return (
 | 
			
		||||
      <Table
 | 
			
		||||
        onClickRow={(row) => {
 | 
			
		||||
                    window.open(row.url, '_blank');
 | 
			
		||||
          window.open(row.url, "_blank");
 | 
			
		||||
        }}
 | 
			
		||||
        tableChildProps={{
 | 
			
		||||
          trbody: {
 | 
			
		||||
                        className: 'cursor-pointer',
 | 
			
		||||
            className: "cursor-pointer",
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        actionsOptions={{
 | 
			
		||||
          actions: [
 | 
			
		||||
            {
 | 
			
		||||
                            key: 'add',
 | 
			
		||||
                            title: 'Add',
 | 
			
		||||
              key: "add",
 | 
			
		||||
              title: "Add",
 | 
			
		||||
              callback: () => {
 | 
			
		||||
                bidModal.open();
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                            key: 'delete',
 | 
			
		||||
                            title: 'Delete',
 | 
			
		||||
              key: "delete",
 | 
			
		||||
              title: "Delete",
 | 
			
		||||
              callback: (data) => {
 | 
			
		||||
                if (!data.length) return;
 | 
			
		||||
                setConfirm({
 | 
			
		||||
                                    title: 'Delete',
 | 
			
		||||
                  title: "Delete",
 | 
			
		||||
                  message: `${data.length} will be delete`,
 | 
			
		||||
                  handleOk: async () => {
 | 
			
		||||
                    const result = await deletesBid(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -204,18 +231,18 @@ export default function Bids() {
 | 
			
		|||
        showLoading={true}
 | 
			
		||||
        highlightOnHover
 | 
			
		||||
        styleDefaultHead={{
 | 
			
		||||
                    justifyContent: 'flex-start',
 | 
			
		||||
                    width: 'fit-content',
 | 
			
		||||
          justifyContent: "flex-start",
 | 
			
		||||
          width: "fit-content",
 | 
			
		||||
        }}
 | 
			
		||||
        options={{
 | 
			
		||||
          query: getBids,
 | 
			
		||||
                    pathToData: 'data.data',
 | 
			
		||||
          pathToData: "data.data",
 | 
			
		||||
          keyOptions: {
 | 
			
		||||
                        last_page: 'lastPage',
 | 
			
		||||
                        per_page: 'perPage',
 | 
			
		||||
                        from: 'from',
 | 
			
		||||
                        to: 'to',
 | 
			
		||||
                        total: 'total',
 | 
			
		||||
            last_page: "lastPage",
 | 
			
		||||
            per_page: "perPage",
 | 
			
		||||
            from: "from",
 | 
			
		||||
            to: "to",
 | 
			
		||||
            total: "total",
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        rows={[]}
 | 
			
		||||
| 
						 | 
				
			
			@ -229,7 +256,10 @@ export default function Bids() {
 | 
			
		|||
            return (
 | 
			
		||||
              <Menu shadow="md" width={200}>
 | 
			
		||||
                <Menu.Target>
 | 
			
		||||
                                    <Box onClick={(e) => e.stopPropagation()} className="flex w-full items-center justify-center">
 | 
			
		||||
                  <Box
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                    className="flex w-full items-center justify-center"
 | 
			
		||||
                  >
 | 
			
		||||
                    <ActionIcon size="sm" variant="light">
 | 
			
		||||
                      <IconMenu size={14} />
 | 
			
		||||
                    </ActionIcon>
 | 
			
		||||
| 
						 | 
				
			
			@ -256,11 +286,23 @@ export default function Bids() {
 | 
			
		|||
                  >
 | 
			
		||||
                    Histories
 | 
			
		||||
                  </Menu.Item>
 | 
			
		||||
                                    {['https://www.grays.com'].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();
 | 
			
		||||
                          }
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                      leftSection={<IconHammer size={14} />}
 | 
			
		||||
                    >
 | 
			
		||||
| 
						 | 
				
			
			@ -269,14 +311,23 @@ export default function Bids() {
 | 
			
		|||
                  )}
 | 
			
		||||
 | 
			
		||||
                  <Menu.Item
 | 
			
		||||
                                        disabled={row.status === 'win-bid'}
 | 
			
		||||
                    disabled={row.status === "win-bid"}
 | 
			
		||||
                    onClick={() => handleToggleBid(row)}
 | 
			
		||||
                                        leftSection={row.status === 'biding' ? <IconAdOff size={14} /> : <IconAd size={14} />}
 | 
			
		||||
                    leftSection={
 | 
			
		||||
                      row.status === "biding" ? (
 | 
			
		||||
                        <IconAdOff size={14} />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <IconAd size={14} />
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                                        {row.status === 'biding' ? 'Disable' : 'Enable'}
 | 
			
		||||
                    {row.status === "biding" ? "Disable" : "Enable"}
 | 
			
		||||
                  </Menu.Item>
 | 
			
		||||
 | 
			
		||||
                                    <Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
 | 
			
		||||
                  <Menu.Item
 | 
			
		||||
                    onClick={() => handleDelete(row)}
 | 
			
		||||
                    leftSection={<IconTrash color="red" size={14} />}
 | 
			
		||||
                  >
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </Menu.Item>
 | 
			
		||||
                </Menu.Dropdown>
 | 
			
		||||
| 
						 | 
				
			
			@ -293,7 +344,6 @@ export default function Bids() {
 | 
			
		|||
  return (
 | 
			
		||||
    <Box>
 | 
			
		||||
      {table}
 | 
			
		||||
 | 
			
		||||
      <ShowHistoriesModal
 | 
			
		||||
        opened={openedHistories}
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -318,7 +368,8 @@ export default function Bids() {
 | 
			
		|||
        }}
 | 
			
		||||
        data={clickData}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {/* Grays */}
 | 
			
		||||
      {openedHistoriesGraysApi && (
 | 
			
		||||
        <ShowHistoriesBidGraysApiModal
 | 
			
		||||
          onUpdated={() => {
 | 
			
		||||
            if (refTableFn.current?.fetchData) {
 | 
			
		||||
| 
						 | 
				
			
			@ -335,6 +386,25 @@ export default function Bids() {
 | 
			
		|||
          }}
 | 
			
		||||
          data={clickData}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {openedHistoriesPicklesApi && (
 | 
			
		||||
        <ShowHistoriesBidPicklesApiModal
 | 
			
		||||
          onUpdated={() => {
 | 
			
		||||
            if (refTableFn.current?.fetchData) {
 | 
			
		||||
              refTableFn.current.fetchData();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setClickData(null);
 | 
			
		||||
          }}
 | 
			
		||||
          opened={true}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            historiesPicklesApiModel.close();
 | 
			
		||||
            setClickData(null);
 | 
			
		||||
          }}
 | 
			
		||||
          data={clickData}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +1,48 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import { Box, Button, LoadingOverlay, Text, Title } from '@mantine/core';
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import io from 'socket.io-client';
 | 
			
		||||
import { WorkingPage } from '../components/dashboard';
 | 
			
		||||
import { IBid, IWebBid } from '../system/type';
 | 
			
		||||
import { checkStatus } from '../apis/auth';
 | 
			
		||||
import { IconPower, IconRestore } from '@tabler/icons-react';
 | 
			
		||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
			
		||||
import { resetTool, shutdownTool } from '../apis/dashboard';
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  LoadingOverlay,
 | 
			
		||||
  Text,
 | 
			
		||||
  Title,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
} from "@mantine/core";
 | 
			
		||||
import { useEffect, useRef, useState } from "react";
 | 
			
		||||
import io from "socket.io-client";
 | 
			
		||||
import { WorkingPage } from "../components/dashboard";
 | 
			
		||||
import { IBid, IWebBid } from "../system/type";
 | 
			
		||||
import { checkStatus } from "../apis/auth";
 | 
			
		||||
import { IconPower, IconRestore } from "@tabler/icons-react";
 | 
			
		||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
			
		||||
import { getStatusTool, resetTool, shutdownTool } from "../apis/dashboard";
 | 
			
		||||
import { cn } from "../utils";
 | 
			
		||||
import { useStatusToolStore } from "../lib/zustand/use-status-tool-store";
 | 
			
		||||
 | 
			
		||||
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
 | 
			
		||||
  autoConnect: true,
 | 
			
		||||
    transports: ['websocket'],
 | 
			
		||||
  transports: ["websocket"],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default function DashBoard() {
 | 
			
		||||
    const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
 | 
			
		||||
  const [workingData, setWorkingData] = useState<
 | 
			
		||||
    (IWebBid & { type: string })[] | (IBid & { type: string })[]
 | 
			
		||||
  >([]);
 | 
			
		||||
  const { setConfirm } = useConfirmStore();
 | 
			
		||||
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const { setStatusTool, statusTool } = useStatusToolStore();
 | 
			
		||||
 | 
			
		||||
  const RETRY_CONNECT = useRef(2);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    socket.connect();
 | 
			
		||||
 | 
			
		||||
        socket.on('connect', () => {
 | 
			
		||||
            socket.emit('getBidsData');
 | 
			
		||||
    socket.on("connect", () => {
 | 
			
		||||
      socket.emit("getBidsData");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        socket.on('disconnect', async () => {
 | 
			
		||||
    socket.on("disconnect", async () => {
 | 
			
		||||
      if (RETRY_CONNECT.current > 0) {
 | 
			
		||||
        await checkStatus();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +53,7 @@ export default function DashBoard() {
 | 
			
		|||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        socket.on('adminBidsUpdated', (data: IWebBid[]) => {
 | 
			
		||||
    socket.on("adminBidsUpdated", (data: IWebBid[]) => {
 | 
			
		||||
      const array = data.reduce((prev, cur) => {
 | 
			
		||||
        if (cur.children?.length > 0) {
 | 
			
		||||
          prev = [...prev, ...cur.children];
 | 
			
		||||
| 
						 | 
				
			
			@ -53,28 +66,46 @@ export default function DashBoard() {
 | 
			
		|||
        if (item.children) {
 | 
			
		||||
          return {
 | 
			
		||||
            ...item,
 | 
			
		||||
                        type: 'API_BID',
 | 
			
		||||
            type: "API_BID",
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          ...item,
 | 
			
		||||
                    type: 'PRODUCT_TAB',
 | 
			
		||||
          type: "PRODUCT_TAB",
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
      setWorkingData(newData);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
            console.log('🔌 Cleanup WebSocket listeners...');
 | 
			
		||||
            socket.off('adminBidsUpdated');
 | 
			
		||||
            socket.off('working');
 | 
			
		||||
            socket.off('connect');
 | 
			
		||||
            socket.off('disconnect');
 | 
			
		||||
      console.log("🔌 Cleanup WebSocket listeners...");
 | 
			
		||||
      socket.off("adminBidsUpdated");
 | 
			
		||||
      socket.off("working");
 | 
			
		||||
      socket.off("connect");
 | 
			
		||||
      socket.off("disconnect");
 | 
			
		||||
      socket.disconnect();
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const statusTool = async () => {
 | 
			
		||||
      const result = await getStatusTool();
 | 
			
		||||
 | 
			
		||||
      if (result?.data) {
 | 
			
		||||
        setStatusTool(result?.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        setStatusTool(false);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const intervalId = setInterval(statusTool, 5000);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearInterval(intervalId);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleResetTool = () => {
 | 
			
		||||
    setConfirm({
 | 
			
		||||
      handleOk: async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,9 +113,10 @@ export default function DashBoard() {
 | 
			
		|||
        await resetTool();
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      },
 | 
			
		||||
            title: 'Confirm tool reset',
 | 
			
		||||
            message: 'Are you sure you want to reset this tool? All current processes will be stopped and restarted.',
 | 
			
		||||
            okButton: { value: 'Ok', color: 'blue' },
 | 
			
		||||
      title: "Confirm tool reset",
 | 
			
		||||
      message:
 | 
			
		||||
        "Are you sure you want to reset this tool? All current processes will be stopped and restarted.",
 | 
			
		||||
      okButton: { value: "Ok", color: "blue" },
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -95,9 +127,10 @@ export default function DashBoard() {
 | 
			
		|||
        await shutdownTool();
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      },
 | 
			
		||||
            title: 'Confirm tool shutdown',
 | 
			
		||||
            message: 'Are you sure you want to shut down this tool? All running processes will be stopped and the tool will go offline.',
 | 
			
		||||
            okButton: { value: 'Ok', color: 'blue' },
 | 
			
		||||
      title: "Confirm tool shutdown",
 | 
			
		||||
      message:
 | 
			
		||||
        "Are you sure you want to shut down this tool? All running processes will be stopped and the tool will go offline.",
 | 
			
		||||
      okButton: { value: "Ok", color: "blue" },
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -107,17 +140,38 @@ export default function DashBoard() {
 | 
			
		|||
        <Title order={2} mb="md">
 | 
			
		||||
          Admin Dashboard
 | 
			
		||||
        </Title>
 | 
			
		||||
                <Box className="flex gap-2">
 | 
			
		||||
                    <Button onClick={handleResetTool} leftSection={<IconRestore size={16} />} size="xs">
 | 
			
		||||
                        Reset tool
 | 
			
		||||
 | 
			
		||||
        <Tooltip label={typeof statusTool === "string" && statusTool}>
 | 
			
		||||
          <Box
 | 
			
		||||
            className={cn("flex gap-2 border  py-3 px-4 rounded-md", {
 | 
			
		||||
              ["border-green-800"]: statusTool || statusTool === "online",
 | 
			
		||||
              ["border-red-800"]: !statusTool || statusTool !== "online",
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              color={statusTool === "online" ? "blue" : "green"}
 | 
			
		||||
              onClick={handleResetTool}
 | 
			
		||||
              leftSection={<IconRestore size={16} />}
 | 
			
		||||
              size="xs"
 | 
			
		||||
            >
 | 
			
		||||
              {statusTool === "online" ? "Reset tool" : "Start tool"}
 | 
			
		||||
            </Button>
 | 
			
		||||
                    <Button onClick={handleShutdownTool} leftSection={<IconPower size={16} />} color="red" size="xs">
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={handleShutdownTool}
 | 
			
		||||
              leftSection={<IconPower size={16} />}
 | 
			
		||||
              color="red"
 | 
			
		||||
              size="xs"
 | 
			
		||||
            >
 | 
			
		||||
              Shutdown tool
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      </Box>
 | 
			
		||||
            <Box className="grid grid-cols-4 gap-4">
 | 
			
		||||
                {workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
 | 
			
		||||
      <Box className="grid grid-cols-4 gap-4 mt-5">
 | 
			
		||||
        {workingData.length > 0 &&
 | 
			
		||||
          workingData.map((item, index) => (
 | 
			
		||||
            <WorkingPage socket={socket} data={item} key={item.id + index} />
 | 
			
		||||
          ))}
 | 
			
		||||
 | 
			
		||||
        {workingData.length <= 0 && (
 | 
			
		||||
          <Box className="flex items-center justify-center col-span-4">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,21 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import { clsx, type ClassValue } from 'clsx';
 | 
			
		||||
import { twMerge } from 'tailwind-merge';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import { clsx, type ClassValue } from "clsx";
 | 
			
		||||
import { twMerge } from "tailwind-merge";
 | 
			
		||||
import moment from "moment";
 | 
			
		||||
import { IWebBid } from "../system/type";
 | 
			
		||||
export function cn(...args: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(args));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const formatTime = (time: string, patent = 'DD/MM/YYYY') => {
 | 
			
		||||
export const formatTime = (time: string, patent = "DD/MM/YYYY") => {
 | 
			
		||||
  return moment(time).format(patent);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function removeFalsyValues<T extends Record<string, any>>(obj: T, excludeKeys: (keyof T)[] = []): Partial<T> {
 | 
			
		||||
export function removeFalsyValues<T extends Record<string, any>>(
 | 
			
		||||
  obj: T,
 | 
			
		||||
  excludeKeys: (keyof T)[] = []
 | 
			
		||||
): Partial<T> {
 | 
			
		||||
  return Object.entries(obj).reduce((acc, [key, value]) => {
 | 
			
		||||
    if (value || excludeKeys.includes(key as keyof T)) {
 | 
			
		||||
      acc[key as keyof T] = value;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,18 +38,18 @@ export function isValidJSON(str: string): boolean {
 | 
			
		|||
 | 
			
		||||
export function copyToClipboard(text: string, onSuccess?: () => void): void {
 | 
			
		||||
  if (!navigator.clipboard) {
 | 
			
		||||
        const textarea = document.createElement('textarea');
 | 
			
		||||
    const textarea = document.createElement("textarea");
 | 
			
		||||
    textarea.value = text;
 | 
			
		||||
        textarea.style.position = 'fixed';
 | 
			
		||||
    textarea.style.position = "fixed";
 | 
			
		||||
    document.body.appendChild(textarea);
 | 
			
		||||
    textarea.focus();
 | 
			
		||||
    textarea.select();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
            document.execCommand('copy');
 | 
			
		||||
      document.execCommand("copy");
 | 
			
		||||
      if (onSuccess) onSuccess();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
            console.error('Không thể copy nội dung: ', err);
 | 
			
		||||
      console.error("Không thể copy nội dung: ", err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    document.body.removeChild(textarea);
 | 
			
		||||
| 
						 | 
				
			
			@ -55,16 +59,16 @@ export function copyToClipboard(text: string, onSuccess?: () => void): void {
 | 
			
		|||
      .then(() => {
 | 
			
		||||
        if (onSuccess) onSuccess();
 | 
			
		||||
      })
 | 
			
		||||
            .catch((err) => console.error('Lỗi khi copy nội dung: ', err));
 | 
			
		||||
      .catch((err) => console.error("Lỗi khi copy nội dung: ", err));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function base64ToFile(base64String: string, fileName: string): File {
 | 
			
		||||
    const [header, base64Content] = base64String.split(',');
 | 
			
		||||
  const [header, base64Content] = base64String.split(",");
 | 
			
		||||
 | 
			
		||||
  const mimeTypeMatch = header.match(/:(.*?);/);
 | 
			
		||||
  if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
 | 
			
		||||
        throw new Error('Invalid base64 string');
 | 
			
		||||
    throw new Error("Invalid base64 string");
 | 
			
		||||
  }
 | 
			
		||||
  const mimeType = mimeTypeMatch[1];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -79,24 +83,27 @@ export function base64ToFile(base64String: string, fileName: string): File {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function toSlug(str: string, maxLength = 60): string {
 | 
			
		||||
    if (typeof str !== 'string') return ''; // Kiểm tra giá trị đầu vào
 | 
			
		||||
  if (typeof str !== "string") return ""; // Kiểm tra giá trị đầu vào
 | 
			
		||||
 | 
			
		||||
  // Kiểm tra nếu môi trường hỗ trợ `normalize`
 | 
			
		||||
    const normalizedStr = str.normalize ? str.normalize('NFD') : str;
 | 
			
		||||
  const normalizedStr = str.normalize ? str.normalize("NFD") : str;
 | 
			
		||||
 | 
			
		||||
  return normalizedStr
 | 
			
		||||
        .replace(/[\u0300-\u036f]/g, '') // Xóa dấu
 | 
			
		||||
        .replace(/[^a-zA-Z0-9\s-]/g, '') // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
 | 
			
		||||
    .replace(/[\u0300-\u036f]/g, "") // Xóa dấu
 | 
			
		||||
    .replace(/[^a-zA-Z0-9\s-]/g, "") // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
 | 
			
		||||
    .trim() // Xóa khoảng trắng đầu/cuối
 | 
			
		||||
        .replace(/\s+/g, '-') // Thay khoảng trắng bằng "-"
 | 
			
		||||
        .replace(/-+/g, '-') // Gộp nhiều dấu "-" thành 1
 | 
			
		||||
    .replace(/\s+/g, "-") // Thay khoảng trắng bằng "-"
 | 
			
		||||
    .replace(/-+/g, "-") // Gộp nhiều dấu "-" thành 1
 | 
			
		||||
    .toLowerCase() // Chuyển về chữ thường
 | 
			
		||||
    .slice(0, maxLength) // Giới hạn độ dài
 | 
			
		||||
        .replace(/^-+|-+$/g, ''); // Xóa "-" đầu/cuối
 | 
			
		||||
    .replace(/^-+|-+$/g, ""); // Xóa "-" đầu/cuối
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function estimateReadingTimeInSeconds(content: string, wordsPerMinute = 200): number {
 | 
			
		||||
    if (!content || typeof content !== 'string') return 0;
 | 
			
		||||
export function estimateReadingTimeInSeconds(
 | 
			
		||||
  content: string,
 | 
			
		||||
  wordsPerMinute = 200
 | 
			
		||||
): number {
 | 
			
		||||
  if (!content || typeof content !== "string") return 0;
 | 
			
		||||
 | 
			
		||||
  const wordCount = content.trim().split(/\s+/).length;
 | 
			
		||||
  return Math.ceil((wordCount / wordsPerMinute) * 60);
 | 
			
		||||
| 
						 | 
				
			
			@ -110,3 +117,62 @@ export function extractDomain(url: string): string | null {
 | 
			
		|||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hash chuỗi thành số nguyên
 | 
			
		||||
export function hashStringToInt(str: string): number {
 | 
			
		||||
  let hash = 0;
 | 
			
		||||
  for (let i = 0; i < str.length; i++) {
 | 
			
		||||
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
 | 
			
		||||
    hash = hash & hash; // convert to 32bit integer
 | 
			
		||||
  }
 | 
			
		||||
  return Math.abs(hash);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Biến số thành màu HEX
 | 
			
		||||
export function intToHexColor(int: number): string {
 | 
			
		||||
  const r = (int >> 16) & 0xff;
 | 
			
		||||
  const g = (int >> 8) & 0xff;
 | 
			
		||||
  const b = int & 0xff;
 | 
			
		||||
  return `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringToColor(str: string): string {
 | 
			
		||||
  const colorPalette = [
 | 
			
		||||
    "#FF6B6B",
 | 
			
		||||
    "#FFD93D",
 | 
			
		||||
    "#FF9F1C",
 | 
			
		||||
    "#F76C6C",
 | 
			
		||||
    "#6BCB77",
 | 
			
		||||
    "#4ECDC4",
 | 
			
		||||
    "#F7B801",
 | 
			
		||||
    "#FF6F91",
 | 
			
		||||
    "#00C9A7",
 | 
			
		||||
  ];
 | 
			
		||||
  const hash = hashStringToInt(str);
 | 
			
		||||
  const index = hash % colorPalette.length;
 | 
			
		||||
  return colorPalette[index];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1,5 @@
 | 
			
		|||
{"createdAt":1744861741554}
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
{"createdAt":1745827424853}
 | 
			
		||||
=======
 | 
			
		||||
{"createdAt":1746413672600}
 | 
			
		||||
>>>>>>> 26b10a7 (pickxel and fix login)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,11 +2,27 @@ 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}¤cyCode=AUD`,
 | 
			
		||||
          });
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +30,28 @@ export class GraysApi {
 | 
			
		|||
          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([]);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { Controller, Post } from '@nestjs/common';
 | 
			
		||||
import { Controller, Get, Post } from '@nestjs/common';
 | 
			
		||||
import { DashboardService } from '../../services/dashboard.service';
 | 
			
		||||
 | 
			
		||||
@Controller('admin/dashboards')
 | 
			
		||||
| 
						 | 
				
			
			@ -14,4 +14,9 @@ export class AdminDashboardController {
 | 
			
		|||
  async shutdownTool() {
 | 
			
		||||
    return await this.dashboardService.shutdownTool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Get('status-tool')
 | 
			
		||||
  async statusTool() {
 | 
			
		||||
    return await this.dashboardService.statusTool();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ import { BidsService } from '../../services/bids.service';
 | 
			
		|||
import { WebBidsService } from '../../services/web-bids.service';
 | 
			
		||||
import { Event } from '../../utils/events';
 | 
			
		||||
import AppResponse from '@/response/app-response';
 | 
			
		||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
 | 
			
		||||
 | 
			
		||||
@Controller('bids')
 | 
			
		||||
export class BidsController {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,11 +69,18 @@ export class BidsController {
 | 
			
		|||
    return this.bidsService.updateStatusWork(id, type, image);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('update-login-status')
 | 
			
		||||
  async updateLoginStatus(
 | 
			
		||||
    @Body() data: ClientUpdateLoginStatusDto
 | 
			
		||||
  ) {
 | 
			
		||||
    return await this.bidsService.emitLoginStatus(data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Post('test')
 | 
			
		||||
  async test(@Body('code') code: string) {
 | 
			
		||||
    const webBid = await this.webBidService.webBidRepo.findOne({
 | 
			
		||||
      // where: { id: 9 },
 | 
			
		||||
      where: { id: 8 },
 | 
			
		||||
      // where: { id: 4 },
 | 
			
		||||
      where: { id: 1 },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.emit(Event.verifyCode(webBid), {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
import { IsBoolean, IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
 | 
			
		||||
import { WebBid } from '../../entities/wed-bid.entity';
 | 
			
		||||
 | 
			
		||||
export class ClientUpdateLoginStatusDto {
 | 
			
		||||
 | 
			
		||||
    @IsObject()
 | 
			
		||||
    data: WebBid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @IsBoolean()
 | 
			
		||||
    login_status: boolean
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,17 @@ export class AdminBidGateway implements OnGatewayConnection {
 | 
			
		|||
      this.server.emit(Event.WORKING, data);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.onAny(
 | 
			
		||||
      (
 | 
			
		||||
        event: string,
 | 
			
		||||
        payload: { login_status: string; data: WebBid },
 | 
			
		||||
      ) => {
 | 
			
		||||
        if (!event.startsWith(Event.LOGIN_STATUS)) return;
 | 
			
		||||
 | 
			
		||||
        this.server.emit(Event.statusLogin(payload.data), payload);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // IMAP
 | 
			
		||||
    this.imapService.connectIMAP();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ import { WebBidsService } from './web-bids.service';
 | 
			
		|||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
			
		||||
import { Event } from '../utils/events';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class BidsService {
 | 
			
		||||
| 
						 | 
				
			
			@ -227,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)
 | 
			
		||||
| 
						 | 
				
			
			@ -273,7 +274,7 @@ export class BidsService {
 | 
			
		|||
    const result = await this.bidsRepo.save({
 | 
			
		||||
      ...bid,
 | 
			
		||||
      ...data,
 | 
			
		||||
      current_price: Math.max(data.current_price, bid.current_price),
 | 
			
		||||
      current_price: Math.max(data?.current_price || 0,  bid.current_price),
 | 
			
		||||
      updated_at: new Date(), // Cập nhật timestamp
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -508,4 +509,12 @@ export class BidsService {
 | 
			
		|||
 | 
			
		||||
    return AppResponse.toResponse(files);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async emitLoginStatus(data: ClientUpdateLoginStatusDto){
 | 
			
		||||
 | 
			
		||||
    this.eventEmitter.emit(Event.statusLogin(data.data), data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return AppResponse.toResponse(true)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ export class DashboardService {
 | 
			
		|||
 | 
			
		||||
  private readonly tool_name = 'auto-bid-tool';
 | 
			
		||||
 | 
			
		||||
  async resetToolByName(toolName: string): Promise<string> {
 | 
			
		||||
  async resetProcessByName(toolName: string): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      // Lấy danh sách process đang chạy
 | 
			
		||||
      exec('pm2 jlist', (error, stdout, stderr) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +41,35 @@ export class DashboardService {
 | 
			
		|||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async shutdownToolByName(toolName: string): Promise<string> {
 | 
			
		||||
  async getStatusProcessByName(toolName: string): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      exec('pm2 jlist', (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          return reject(`Error get list process: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
  
 | 
			
		||||
        try {
 | 
			
		||||
          const processList = JSON.parse(stdout);
 | 
			
		||||
          const targetProcess = processList.find(
 | 
			
		||||
            (proc: any) => proc.name === toolName,
 | 
			
		||||
          );
 | 
			
		||||
  
 | 
			
		||||
          if (!targetProcess) {
 | 
			
		||||
            return reject(`Not found process for name "${toolName}"`);
 | 
			
		||||
          }
 | 
			
		||||
  
 | 
			
		||||
          const status = targetProcess.pm2_env?.status || 'unknown';
 | 
			
		||||
          return resolve(status); // Trả về: 'online', 'stopped', 'errored', etc.
 | 
			
		||||
  
 | 
			
		||||
        } catch (parseErr) {
 | 
			
		||||
          reject(`Error parse JSON output: ${parseErr}`);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  async shutdownProcessByName(toolName: string): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      // Lấy danh sách process đang chạy
 | 
			
		||||
      exec('pm2 jlist', (error, stdout, stderr) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +105,7 @@ export class DashboardService {
 | 
			
		|||
 | 
			
		||||
  async resetTool() {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.resetToolByName(this.tool_name);
 | 
			
		||||
      await this.resetProcessByName(this.tool_name);
 | 
			
		||||
 | 
			
		||||
      return AppResponse.toResponse(true);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -87,11 +115,22 @@ export class DashboardService {
 | 
			
		|||
 | 
			
		||||
  async shutdownTool() {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.shutdownToolByName(this.tool_name);
 | 
			
		||||
      await this.shutdownProcessByName(this.tool_name);
 | 
			
		||||
 | 
			
		||||
      return AppResponse.toResponse(true);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return AppResponse.toResponse(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  async statusTool() {
 | 
			
		||||
    try {
 | 
			
		||||
     const result = await this.getStatusProcessByName(this.tool_name);
 | 
			
		||||
 | 
			
		||||
      return AppResponse.toResponse(result);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return AppResponse.toResponse(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,13 @@ export class Event {
 | 
			
		|||
  public static BIDS_UPDATED = 'bidsUpdated';
 | 
			
		||||
  public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
 | 
			
		||||
  public static WEB_UPDATED = 'webUpdated';
 | 
			
		||||
  public static LOGIN_STATUS = 'login-status';
 | 
			
		||||
 | 
			
		||||
  public static verifyCode(data: WebBid) {
 | 
			
		||||
    return `${this.VERIFY_CODE}.${data.origin_url}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static statusLogin(data: WebBid) {
 | 
			
		||||
    return `${this.LOGIN_STATUS}.${data.origin_url}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		|||
    TypeOrmModule.forRootAsync({
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
      useFactory: (configService: ConfigService) => ({
 | 
			
		||||
      useFactory: (configService: ConfigService) => {
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'mysql',
 | 
			
		||||
          host: configService.get<string>('DB_HOST'),
 | 
			
		||||
          port: configService.get<number>('DB_PORT'),
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +20,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		|||
          entities: ['dist/**/*.entity{.ts,.js}'],
 | 
			
		||||
          synchronize:
 | 
			
		||||
            configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
 | 
			
		||||
      }),
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,10 @@ export function extractModelId(url: string): string | null {
 | 
			
		|||
      const match = url.split('_');
 | 
			
		||||
      return match ? match[1] : null;
 | 
			
		||||
    }
 | 
			
		||||
    case 'https://www.pickles.com.au': {
 | 
			
		||||
      const model = url.split('/').pop();
 | 
			
		||||
      return model ? model : null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  apps: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "auto-bid-tool",
 | 
			
		||||
      script: "./index.js",
 | 
			
		||||
      instances: 1,
 | 
			
		||||
      exec_mode: "fork",
 | 
			
		||||
      watch: false,
 | 
			
		||||
      log_date_format: "YYYY-MM-DD HH:mm:ss",
 | 
			
		||||
      output: "./logs/out.log",
 | 
			
		||||
      error: "./logs/error.log",
 | 
			
		||||
      merge_logs: true,
 | 
			
		||||
      max_memory_restart: "12G",
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +1,22 @@
 | 
			
		|||
import 'dotenv/config';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import pLimit from 'p-limit';
 | 
			
		||||
import { io } from 'socket.io-client';
 | 
			
		||||
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } 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 "dotenv/config";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
import pLimit from "p-limit";
 | 
			
		||||
import { io } from "socket.io-client";
 | 
			
		||||
import {
 | 
			
		||||
  createApiBid,
 | 
			
		||||
  createBidProduct,
 | 
			
		||||
  deleteProfile,
 | 
			
		||||
  shouldUpdateProductTab,
 | 
			
		||||
} from "./service/app-service.js";
 | 
			
		||||
import browser from "./system/browser.js";
 | 
			
		||||
import configs from "./system/config.js";
 | 
			
		||||
import {
 | 
			
		||||
  delay,
 | 
			
		||||
  findEarlyLoginTime,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  safeClosePage,
 | 
			
		||||
} from "./system/utils.js";
 | 
			
		||||
import { updateLoginStatus } from "./system/apis/bid.js";
 | 
			
		||||
 | 
			
		||||
global.IS_CLEANING = true;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +26,7 @@ const activeTasks = new Set();
 | 
			
		|||
 | 
			
		||||
const handleUpdateProductTabs = (data) => {
 | 
			
		||||
  if (!Array.isArray(data)) {
 | 
			
		||||
        console.log('Data must be array');
 | 
			
		||||
    console.log("Data must be array");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,19 +58,86 @@ 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...');
 | 
			
		||||
  console.log("🚀 Tracking process started...");
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    try {
 | 
			
		||||
            console.log('🔍 Scanning active bids...');
 | 
			
		||||
            const productTabs = _.flatMap(MANAGER_BIDS, 'children');
 | 
			
		||||
      console.log("🔍 Scanning active bids...");
 | 
			
		||||
      const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
			
		||||
 | 
			
		||||
      await Promise.allSettled(
 | 
			
		||||
        MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
 | 
			
		||||
          console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
 | 
			
		||||
          return apiBid.listen_events();
 | 
			
		||||
                }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      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(
 | 
			
		||||
| 
						 | 
				
			
			@ -71,20 +149,26 @@ const tracking = async () => {
 | 
			
		|||
            const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
 | 
			
		||||
            productTab.parent_browser_context = parent?.browser_context;
 | 
			
		||||
            if (!productTab.parent_browser_context) {
 | 
			
		||||
                            console.log(`⏳ Waiting for parent process... (Product ID: ${productTab.id})`);
 | 
			
		||||
              console.log(
 | 
			
		||||
                `⏳ Waiting for parent process... (Product ID: ${productTab.id})`
 | 
			
		||||
              );
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Kết nối Puppeteer nếu chưa có page_context
 | 
			
		||||
          if (!productTab.page_context) {
 | 
			
		||||
                        console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `🔌 Connecting to page for Product ID: ${productTab.id}`
 | 
			
		||||
            );
 | 
			
		||||
            await productTab.puppeteer_connect();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Kiểm tra URL và điều hướng nếu cần
 | 
			
		||||
          if ((await productTab.page_context.url()) !== productTab.url) {
 | 
			
		||||
                        console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `🔄 Redirecting to new URL for Product ID: ${productTab.id}`
 | 
			
		||||
            );
 | 
			
		||||
            await productTab.gotoLink();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -93,74 +177,159 @@ const tracking = async () => {
 | 
			
		|||
            console.log(`🔄 Updating Product ID: ${productTab.id}...`);
 | 
			
		||||
            await productTab.update();
 | 
			
		||||
          } else {
 | 
			
		||||
                        console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Chờ first bid
 | 
			
		||||
          if (!productTab.first_bid) {
 | 
			
		||||
                        console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `🎯 Waiting for first bid for Product ID: ${productTab.id}`
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Kiểm tra thời gian bid
 | 
			
		||||
                    if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
 | 
			
		||||
                        console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
 | 
			
		||||
          if (
 | 
			
		||||
            productTab.start_bid_time &&
 | 
			
		||||
            !isTimeReached(productTab.start_bid_time)
 | 
			
		||||
          ) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Thực thi hành động
 | 
			
		||||
          console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
 | 
			
		||||
          await productTab.action();
 | 
			
		||||
                }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Dọn dẹp tab không dùng
 | 
			
		||||
            console.log('🧹 Cleaning up unused tabs...');
 | 
			
		||||
      console.log("🧹 Cleaning up unused tabs...");
 | 
			
		||||
      clearLazyTab();
 | 
			
		||||
 | 
			
		||||
      // Cập nhật trạng thái tracking
 | 
			
		||||
            console.log('📊 Tracking work status...');
 | 
			
		||||
      console.log("📊 Tracking work status...");
 | 
			
		||||
      workTracking();
 | 
			
		||||
 | 
			
		||||
      // Bắn event status login
 | 
			
		||||
      console.log("📊 Tracking login status...");
 | 
			
		||||
      trackingLoginStatus();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error('❌ Error in tracking loop:', error);
 | 
			
		||||
      console.error("❌ Error in tracking loop:", error);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
 | 
			
		||||
    console.log(
 | 
			
		||||
      `⏳ Waiting ${
 | 
			
		||||
        configs.AUTO_TRACKING_DELAY / 1000
 | 
			
		||||
      } seconds before the next iteration...`
 | 
			
		||||
    );
 | 
			
		||||
    await delay(configs.AUTO_TRACKING_DELAY);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 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.');
 | 
			
		||||
    console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!browser) {
 | 
			
		||||
        console.warn('⚠️ Browser is not available or disconnected.');
 | 
			
		||||
    console.warn("⚠️ Browser is not available or disconnected.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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()),
 | 
			
		||||
        );
 | 
			
		||||
    const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
 | 
			
		||||
      item.url,
 | 
			
		||||
      ...item.children.map((child) => child.url),
 | 
			
		||||
    ]).filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    for (const page of pages) {
 | 
			
		||||
      try {
 | 
			
		||||
        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
			
		||||
 | 
			
		||||
        const pageUrl = page.url();
 | 
			
		||||
 | 
			
		||||
            // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
 | 
			
		||||
            if (!pageUrl || pageUrl === 'about:blank') continue;
 | 
			
		||||
        if (!pageUrl || pageUrl === "about:blank") continue;
 | 
			
		||||
        if (activeUrls.includes(pageUrl)) continue;
 | 
			
		||||
 | 
			
		||||
        page.removeAllListeners();
 | 
			
		||||
 | 
			
		||||
        console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
			
		||||
 | 
			
		||||
            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(),
 | 
			
		||||
| 
						 | 
				
			
			@ -168,29 +337,39 @@ const clearLazyTab = async () => {
 | 
			
		|||
          }))
 | 
			
		||||
          .find((j) => j.current_url === pageUrl);
 | 
			
		||||
 | 
			
		||||
                        console.log(bidData);
 | 
			
		||||
 | 
			
		||||
        if (bidData && bidData.data) {
 | 
			
		||||
          await safeClosePage(bidData.data);
 | 
			
		||||
        } else {
 | 
			
		||||
                            await page.close();
 | 
			
		||||
          try {
 | 
			
		||||
            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(`🛑 Closing unused tab: ${pageUrl}`);
 | 
			
		||||
                    } catch (err) {
 | 
			
		||||
                        console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        console.log(`✅ Closed page: ${pageUrl}`);
 | 
			
		||||
      } catch (pageErr) {
 | 
			
		||||
        console.warn(`⚠️ Error handling page: ${pageErr.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
        console.error('❌ Error in clearLazyTab:', err.message);
 | 
			
		||||
    console.error("❌ Error in clearLazyTab:", err.message);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const workTracking = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
        const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
 | 
			
		||||
    const activeData = _.flatMap(MANAGER_BIDS, (item) => [
 | 
			
		||||
      item,
 | 
			
		||||
      ...item.children,
 | 
			
		||||
    ]);
 | 
			
		||||
    const limit = pLimit(5);
 | 
			
		||||
 | 
			
		||||
    await Promise.allSettled(
 | 
			
		||||
| 
						 | 
				
			
			@ -203,21 +382,67 @@ const workTracking = async () => {
 | 
			
		|||
            try {
 | 
			
		||||
              await item.handleTakeWorkSnapshot();
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                            console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error);
 | 
			
		||||
              console.error(
 | 
			
		||||
                `[❌ ERROR] Snapshot failed for Product ID: ${item.id}`,
 | 
			
		||||
                error
 | 
			
		||||
              );
 | 
			
		||||
            } finally {
 | 
			
		||||
              activeTasks.delete(item.id);
 | 
			
		||||
            }
 | 
			
		||||
                    }),
 | 
			
		||||
                ),
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack);
 | 
			
		||||
    console.error(
 | 
			
		||||
      `[❌ ERROR] Work tracking failed: ${error.message}\n`,
 | 
			
		||||
      error.stack
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const trackingLoginStatus = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    if (!MANAGER_BIDS?.length) return;
 | 
			
		||||
 | 
			
		||||
    const results = await Promise.allSettled(
 | 
			
		||||
      MANAGER_BIDS.map(async (item) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const login_status = await item.isLogin();
 | 
			
		||||
 | 
			
		||||
          await updateLoginStatus({
 | 
			
		||||
            data: {
 | 
			
		||||
              id: item.id,
 | 
			
		||||
              type: item.type,
 | 
			
		||||
              origin_url: item.origin_url,
 | 
			
		||||
            },
 | 
			
		||||
            login_status,
 | 
			
		||||
          });
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `[⚠️ WARN] Failed to check login for bid ${
 | 
			
		||||
              item?.id || "unknown"
 | 
			
		||||
            }: ${err.message}`
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Optional: log summary
 | 
			
		||||
    const failed = results.filter((r) => r.status === "rejected").length;
 | 
			
		||||
    if (failed) {
 | 
			
		||||
      console.warn(`[⚠️ WARN] ${failed} login status checks failed.`);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(
 | 
			
		||||
      `[❌ ERROR] Login status tracking failed: ${error.message}\n`,
 | 
			
		||||
      error.stack
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
  const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
 | 
			
		||||
        transports: ['websocket'],
 | 
			
		||||
    transports: ["websocket"],
 | 
			
		||||
    reconnection: true,
 | 
			
		||||
    extraHeaders: {
 | 
			
		||||
      Authorization: process.env.CLIENT_KEY,
 | 
			
		||||
| 
						 | 
				
			
			@ -228,30 +453,30 @@ const workTracking = async () => {
 | 
			
		|||
  global.socket = socket;
 | 
			
		||||
 | 
			
		||||
  // listen connect
 | 
			
		||||
    socket.on('connect', () => {
 | 
			
		||||
        console.log('✅ Connected to WebSocket server');
 | 
			
		||||
        console.log('🔗 Socket ID:', socket.id);
 | 
			
		||||
  socket.on("connect", () => {
 | 
			
		||||
    console.log("✅ Connected to WebSocket server");
 | 
			
		||||
    console.log("🔗 Socket ID:", socket.id);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // listen connect
 | 
			
		||||
    socket.on('disconnect', () => {
 | 
			
		||||
        console.log('❌Client key is valid. Disconnected');
 | 
			
		||||
  socket.on("disconnect", () => {
 | 
			
		||||
    console.log("❌Client key is valid. Disconnected");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // listen event
 | 
			
		||||
    socket.on('bidsUpdated', async (data) => {
 | 
			
		||||
        console.log('📢 Bids Data:', data);
 | 
			
		||||
  socket.on("bidsUpdated", async (data) => {
 | 
			
		||||
    console.log("📢 Bids Data:", data);
 | 
			
		||||
 | 
			
		||||
    handleUpdateProductTabs(data);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
    socket.on('webUpdated', async (data) => {
 | 
			
		||||
        console.log('📢 Account was updated:', data);
 | 
			
		||||
  socket.on("webUpdated", async (data) => {
 | 
			
		||||
    console.log("📢 Account was updated:", data);
 | 
			
		||||
 | 
			
		||||
    const isDeleted = deleteProfile(data);
 | 
			
		||||
 | 
			
		||||
    if (isDeleted) {
 | 
			
		||||
            console.log('✅ Profile deleted successfully!');
 | 
			
		||||
      console.log("✅ Profile deleted successfully!");
 | 
			
		||||
 | 
			
		||||
      const tab = MANAGER_BIDS.find((item) => item.url === data.url);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -262,9 +487,13 @@ const workTracking = 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.');
 | 
			
		||||
      console.log("⚠️ No profile found to delete.");
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,15 @@
 | 
			
		|||
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;
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +24,17 @@ export class ApiBid extends Bid {
 | 
			
		|||
  username;
 | 
			
		||||
  password;
 | 
			
		||||
 | 
			
		||||
    constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
 | 
			
		||||
  constructor({
 | 
			
		||||
    url,
 | 
			
		||||
    username,
 | 
			
		||||
    password,
 | 
			
		||||
    id,
 | 
			
		||||
    children,
 | 
			
		||||
    created_at,
 | 
			
		||||
    updated_at,
 | 
			
		||||
    origin_url,
 | 
			
		||||
    active,
 | 
			
		||||
  }) {
 | 
			
		||||
    super(BID_TYPE.API_BID, url);
 | 
			
		||||
 | 
			
		||||
    this.created_at = created_at;
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +47,17 @@ export class ApiBid extends Bid {
 | 
			
		|||
    this.id = id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
 | 
			
		||||
  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;
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +93,9 @@ export class ApiBid extends Bid {
 | 
			
		|||
 | 
			
		||||
    try {
 | 
			
		||||
      const cookies = await this.browser_context.cookies();
 | 
			
		||||
            const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage));
 | 
			
		||||
      const localStorageData = await this.page_context.evaluate(() =>
 | 
			
		||||
        JSON.stringify(localStorage)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const contextData = {
 | 
			
		||||
        cookies,
 | 
			
		||||
| 
						 | 
				
			
			@ -82,10 +109,13 @@ export class ApiBid extends Bid {
 | 
			
		|||
        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!');
 | 
			
		||||
      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);
 | 
			
		||||
      console.log("Save Context: ", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -96,11 +126,19 @@ export class ApiBid extends Bid {
 | 
			
		|||
 | 
			
		||||
    if (!fs.existsSync(filePath)) return;
 | 
			
		||||
 | 
			
		||||
        const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
 | 
			
		||||
    const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
 | 
			
		||||
 | 
			
		||||
    // Restore Cookies
 | 
			
		||||
    await this.page_context.setCookie(...contextData.cookies);
 | 
			
		||||
 | 
			
		||||
        console.log('🔄 Context restored!');
 | 
			
		||||
    console.log("🔄 Context restored!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async onCloseLogin() {}
 | 
			
		||||
 | 
			
		||||
  async isTimeToLogin() {
 | 
			
		||||
    const earlyLoginTime = findEarlyLoginTime(this);
 | 
			
		||||
 | 
			
		||||
    return earlyLoginTime && isTimeReached(earlyLoginTime);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import BID_TYPE from '../system/bid-type.js';
 | 
			
		||||
import CONSTANTS from '../system/constants.js';
 | 
			
		||||
import { takeSnapshot } from '../system/utils.js';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import BID_TYPE from "../system/bid-type.js";
 | 
			
		||||
import CONSTANTS from "../system/constants.js";
 | 
			
		||||
import { takeSnapshot } from "../system/utils.js";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
 | 
			
		||||
export class Bid {
 | 
			
		||||
  type;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,10 +21,24 @@ export class Bid {
 | 
			
		|||
 | 
			
		||||
    try {
 | 
			
		||||
      // await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
 | 
			
		||||
            console.log(`✅ Page fully loaded. Taking snapshot for ${this.type === BID_TYPE.PRODUCT_TAB ? 'Product ID' : 'Tracking ID'}: ${this.id}`);
 | 
			
		||||
            takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `✅ Page fully loaded. Taking snapshot for ${
 | 
			
		||||
          this.type === BID_TYPE.PRODUCT_TAB ? "Product ID" : "Tracking ID"
 | 
			
		||||
        }: ${this.id}`
 | 
			
		||||
      );
 | 
			
		||||
      takeSnapshot(
 | 
			
		||||
        this.page_context,
 | 
			
		||||
        this,
 | 
			
		||||
        "working",
 | 
			
		||||
        CONSTANTS.TYPE_IMAGE.WORK
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
 | 
			
		||||
      console.error(
 | 
			
		||||
        `❌ Error taking snapshot for Product ID: ${this.id}:`,
 | 
			
		||||
        error.message
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }, 1000);
 | 
			
		||||
 | 
			
		||||
  async isLogin() {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,15 @@
 | 
			
		|||
import path from 'path';
 | 
			
		||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
 | 
			
		||||
import configs from '../../system/config.js';
 | 
			
		||||
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
 | 
			
		||||
import { ApiBid } from '../api-bid.js';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import {
 | 
			
		||||
  delay,
 | 
			
		||||
  extractNumber,
 | 
			
		||||
  getPathProfile,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  safeClosePage,
 | 
			
		||||
} from "../../system/utils.js";
 | 
			
		||||
import { ApiBid } from "../api-bid.js";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
 | 
			
		||||
export class GrayApiBid extends ApiBid {
 | 
			
		||||
  retry_login = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +50,9 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
      await page.evaluateHandle(
 | 
			
		||||
        (apiUrl, interval, bidId) => {
 | 
			
		||||
          if (window._autoBidPollingStarted) {
 | 
			
		||||
                        console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `✅ [${bidId}] Polling is already running. Skipping initialization.`
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -52,26 +60,38 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
          window._autoBidPollingStarted = true;
 | 
			
		||||
 | 
			
		||||
          function sendRequest() {
 | 
			
		||||
                        console.log(`📡 [${bidId}] Sending request to track out-bid lots...`);
 | 
			
		||||
            console.log(
 | 
			
		||||
              `📡 [${bidId}] Sending request to track out-bid lots...`
 | 
			
		||||
            );
 | 
			
		||||
            fetch(apiUrl, {
 | 
			
		||||
                            method: 'POST',
 | 
			
		||||
                            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 | 
			
		||||
              method: "POST",
 | 
			
		||||
              headers: { "Content-Type": "application/x-www-form-urlencoded" },
 | 
			
		||||
              body: JSON.stringify({ timeStamp: new Date().getTime() }),
 | 
			
		||||
            })
 | 
			
		||||
                            .then((response) => console.log(`✅ [${bidId}] Response received: ${response.status}`))
 | 
			
		||||
                            .catch((err) => console.error(`⚠️ [${bidId}] Request error:`, err));
 | 
			
		||||
              .then((response) =>
 | 
			
		||||
                console.log(
 | 
			
		||||
                  `✅ [${bidId}] Response received: ${response.status}`
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
              .catch((err) =>
 | 
			
		||||
                console.error(`⚠️ [${bidId}] Request error:`, err)
 | 
			
		||||
              );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          window._pollingInterval = setInterval(sendRequest, interval);
 | 
			
		||||
        },
 | 
			
		||||
        configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
 | 
			
		||||
        configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
 | 
			
		||||
                this.id,
 | 
			
		||||
        this.id
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            if (error.message.includes('Execution context was destroyed')) {
 | 
			
		||||
                console.log(`⚠️ [${this.id}] Page reload detected, restarting polling...`);
 | 
			
		||||
                await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {});
 | 
			
		||||
      if (error.message.includes("Execution context was destroyed")) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⚠️ [${this.id}] Page reload detected, restarting polling...`
 | 
			
		||||
        );
 | 
			
		||||
        await page
 | 
			
		||||
          .waitForNavigation({ waitUntil: "networkidle2" })
 | 
			
		||||
          .catch(() => {});
 | 
			
		||||
        return await this.polling(page);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -101,15 +121,26 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
    // SAVE LOGS ON SERVER
 | 
			
		||||
    this.handleCreateLogsOnServer(data);
 | 
			
		||||
 | 
			
		||||
        const bidOutLots = data.filter((bid) => !this.children_processing.some((item) => item.model === bid.Sku));
 | 
			
		||||
    const bidOutLots = data.filter(
 | 
			
		||||
      (bid) => !this.children_processing.some((item) => item.model === bid.Sku)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        const handleChildren = this.children.filter((item) => bidOutLots.some((i) => i.Sku === item.model));
 | 
			
		||||
    const handleChildren = this.children.filter((item) =>
 | 
			
		||||
      bidOutLots.some((i) => i.Sku === item.model)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        console.log({ handleChildren, children_processing: this.children_processing, data, bidOutLots });
 | 
			
		||||
    console.log({
 | 
			
		||||
      handleChildren,
 | 
			
		||||
      children_processing: this.children_processing,
 | 
			
		||||
      data,
 | 
			
		||||
      bidOutLots,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (const product_tab of handleChildren) {
 | 
			
		||||
      if (!isTimeReached(product_tab.start_bid_time)) {
 | 
			
		||||
                console.log(`❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -121,10 +152,26 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
      await product_tab.action();
 | 
			
		||||
 | 
			
		||||
            this.children_processing = this.children_processing.filter((item) => item.id !== product_tab.id);
 | 
			
		||||
      this.children_processing = this.children_processing.filter(
 | 
			
		||||
        (item) => item.id !== product_tab.id
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  isLogin = async () => {
 | 
			
		||||
    if (!this.page_context) return false;
 | 
			
		||||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
    if (
 | 
			
		||||
      !(await this.page_context.$('input[name="username"]')) ||
 | 
			
		||||
      fs.existsSync(filePath)
 | 
			
		||||
    ) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async handleLogin() {
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -146,11 +193,17 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      await page.type('input[name="username"]', this.username, { delay: 100 });
 | 
			
		||||
      await page.type('input[name="password"]', this.password, { delay: 150 });
 | 
			
		||||
            await page.click('#loginButton');
 | 
			
		||||
      await page.click("#loginButton");
 | 
			
		||||
 | 
			
		||||
      await Promise.race([
 | 
			
		||||
                page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
 | 
			
		||||
                page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
 | 
			
		||||
        page.waitForNavigation({
 | 
			
		||||
          timeout: 8000,
 | 
			
		||||
          waitUntil: "domcontentloaded",
 | 
			
		||||
        }),
 | 
			
		||||
        page.waitForFunction(
 | 
			
		||||
          () => !document.querySelector('input[name="username"]'),
 | 
			
		||||
          { timeout: 8000 }
 | 
			
		||||
        ), // Check if login input disappears
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      if (!(await page.$('input[name="username"]'))) {
 | 
			
		||||
| 
						 | 
				
			
			@ -159,13 +212,19 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            throw new Error('Login failed, login input is still visible.');
 | 
			
		||||
      throw new Error("Login failed, login input is still visible.");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.log(`⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${this.retry_login + 1} ❌`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${
 | 
			
		||||
          this.retry_login + 1
 | 
			
		||||
        } ❌`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.retry_login++;
 | 
			
		||||
      if (this.retry_login > this.retry_login_count) {
 | 
			
		||||
                console.log(`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`
 | 
			
		||||
        );
 | 
			
		||||
        safeClosePage(this);
 | 
			
		||||
        this.retry_login = 0; // Reset retry count
 | 
			
		||||
        return;
 | 
			
		||||
| 
						 | 
				
			
			@ -188,30 +247,37 @@ export class GrayApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
      console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
      console.log(`🎯 [${this.id}] Brought page to front.`);
 | 
			
		||||
 | 
			
		||||
      // Set userAgent
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
      console.log(`🛠️ [${this.id}] UserAgent set.`);
 | 
			
		||||
 | 
			
		||||
            page.on('response', async (response) => {
 | 
			
		||||
                if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
 | 
			
		||||
      page.on("response", async (response) => {
 | 
			
		||||
        if (
 | 
			
		||||
          response.request().url().includes("api/Notifications/GetOutBidLots")
 | 
			
		||||
        ) {
 | 
			
		||||
          console.log(`🚀 [${this.id}] API POST detected: ${response.url()}`);
 | 
			
		||||
 | 
			
		||||
          try {
 | 
			
		||||
            const responseBody = await response.json();
 | 
			
		||||
            await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
                        console.error(`❌ [${this.id}] Error processing response:`, error?.message);
 | 
			
		||||
            console.error(
 | 
			
		||||
              `❌ [${this.id}] Error processing response:`,
 | 
			
		||||
              error?.message
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
            page.on('load', async () => {
 | 
			
		||||
      page.on("load", async () => {
 | 
			
		||||
        console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
 | 
			
		||||
        await this.polling(page);
 | 
			
		||||
        await this.handleLogin();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,20 @@
 | 
			
		|||
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
 | 
			
		||||
import CONSTANTS from '../../system/constants.js';
 | 
			
		||||
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
 | 
			
		||||
import { ProductBid } from '../product-bid.js';
 | 
			
		||||
import {
 | 
			
		||||
  outBid,
 | 
			
		||||
  pushPrice,
 | 
			
		||||
  updateBid,
 | 
			
		||||
  updateStatusByPrice,
 | 
			
		||||
} from "../../system/apis/bid.js";
 | 
			
		||||
import CONSTANTS from "../../system/constants.js";
 | 
			
		||||
import {
 | 
			
		||||
  delay,
 | 
			
		||||
  extractNumber,
 | 
			
		||||
  isNumber,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  removeFalsyValues,
 | 
			
		||||
  safeClosePage,
 | 
			
		||||
  takeSnapshot,
 | 
			
		||||
} from "../../system/utils.js";
 | 
			
		||||
import { ProductBid } from "../product-bid.js";
 | 
			
		||||
 | 
			
		||||
export class GraysProductBid extends ProductBid {
 | 
			
		||||
  constructor({ ...prev }) {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +29,7 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
    if (!isNumber(price_value)) {
 | 
			
		||||
      console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
 | 
			
		||||
            await takeSnapshot(page, this, 'price-value-null');
 | 
			
		||||
      await takeSnapshot(page, this, "price-value-null");
 | 
			
		||||
 | 
			
		||||
      return { result: false, bid_price: 0 };
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -24,8 +37,10 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
    const bid_price = this.plus_price + Number(price_value);
 | 
			
		||||
 | 
			
		||||
    if (bid_price > this.max_price) {
 | 
			
		||||
            console.log(`❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`);
 | 
			
		||||
            await takeSnapshot(page, this, 'price-bid-more-than');
 | 
			
		||||
      console.log(
 | 
			
		||||
        `❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`
 | 
			
		||||
      );
 | 
			
		||||
      await takeSnapshot(page, this, "price-bid-more-than");
 | 
			
		||||
 | 
			
		||||
      await outBid(this.id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,9 +70,14 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('#lot-closing-datetime', { timeout: 3000 });
 | 
			
		||||
      await this.page_context.waitForSelector("#lot-closing-datetime", {
 | 
			
		||||
        timeout: 3000,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
            return await this.page_context.$eval('#lot-closing-datetime', (el) => el.value);
 | 
			
		||||
      return await this.page_context.$eval(
 | 
			
		||||
        "#lot-closing-datetime",
 | 
			
		||||
        (el) => el.value
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -67,11 +87,19 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('#biddableLot form div div:nth-child(1) span span', { timeout: 3000 });
 | 
			
		||||
      await this.page_context.waitForSelector(
 | 
			
		||||
        "#biddableLot form div div:nth-child(1) span span",
 | 
			
		||||
        { timeout: 3000 }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
            const element = await this.page_context.$('#biddableLot form div div:nth-child(1) span span');
 | 
			
		||||
      const element = await this.page_context.$(
 | 
			
		||||
        "#biddableLot form div div:nth-child(1) span span"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
            const textPrice = await this.page_context.evaluate((el) => el.textContent, element);
 | 
			
		||||
      const textPrice = await this.page_context.evaluate(
 | 
			
		||||
        (el) => el.textContent,
 | 
			
		||||
        element
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return extractNumber(textPrice) || null;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -100,31 +128,36 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  async handleWritePrice(page, bid_price) {
 | 
			
		||||
        await page.type('#price', String(bid_price));
 | 
			
		||||
    await page.type("#price", String(bid_price));
 | 
			
		||||
    await delay(500);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async placeBid(page) {
 | 
			
		||||
    try {
 | 
			
		||||
            await page.click('#bid-type-standard');
 | 
			
		||||
      await page.click("#bid-type-standard");
 | 
			
		||||
      await delay(500);
 | 
			
		||||
 | 
			
		||||
            await page.click('#btnSubmit');
 | 
			
		||||
      await page.click("#btnSubmit");
 | 
			
		||||
      await delay(1000);
 | 
			
		||||
 | 
			
		||||
            await page.waitForSelector('button', { timeout: 5000 });
 | 
			
		||||
      await page.waitForSelector("button", { timeout: 5000 });
 | 
			
		||||
 | 
			
		||||
      await delay(500);
 | 
			
		||||
 | 
			
		||||
            await page.click('button');
 | 
			
		||||
      await page.click("button");
 | 
			
		||||
 | 
			
		||||
      await page.waitForNavigation({ timeout: 5000 });
 | 
			
		||||
 | 
			
		||||
            await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
 | 
			
		||||
      await takeSnapshot(
 | 
			
		||||
        page,
 | 
			
		||||
        this,
 | 
			
		||||
        "bid-success",
 | 
			
		||||
        CONSTANTS.TYPE_IMAGE.SUCCESS
 | 
			
		||||
      );
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(`❌ [${this.id}] Timeout to loading`);
 | 
			
		||||
            await takeSnapshot(page, this, 'timeout to loading');
 | 
			
		||||
      await takeSnapshot(page, this, "timeout to loading");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -134,8 +167,20 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
    await delay(1000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
 | 
			
		||||
        const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
 | 
			
		||||
  async handleUpdateBid({
 | 
			
		||||
    lot_id,
 | 
			
		||||
    close_time,
 | 
			
		||||
    name,
 | 
			
		||||
    current_price,
 | 
			
		||||
    reserve_price,
 | 
			
		||||
  }) {
 | 
			
		||||
    const response = await updateBid(this.id, {
 | 
			
		||||
      lot_id,
 | 
			
		||||
      close_time,
 | 
			
		||||
      name,
 | 
			
		||||
      current_price,
 | 
			
		||||
      reserve_price: Number(reserve_price) || 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (response) {
 | 
			
		||||
      this.lot_id = response.lot_id;
 | 
			
		||||
| 
						 | 
				
			
			@ -153,23 +198,43 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
      const close_time = await this.getCloseTime();
 | 
			
		||||
 | 
			
		||||
      // Chờ phần tử xuất hiện trước khi lấy giá trị
 | 
			
		||||
            await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
 | 
			
		||||
            const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
 | 
			
		||||
      await page
 | 
			
		||||
        .waitForSelector("#priceValue", { timeout: 5000 })
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
      const price_value = await page
 | 
			
		||||
        .$eval("#priceValue", (el) => el.value)
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
 | 
			
		||||
            await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
 | 
			
		||||
            const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null);
 | 
			
		||||
 | 
			
		||||
            await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null);
 | 
			
		||||
            const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
 | 
			
		||||
      await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
 | 
			
		||||
      const lot_id = await page
 | 
			
		||||
        .$eval("#lotId", (el) => el.value)
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
 | 
			
		||||
      await page
 | 
			
		||||
                .waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 })
 | 
			
		||||
        .waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
 | 
			
		||||
          timeout: 5000,
 | 
			
		||||
        })
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
            const current_price = await page
 | 
			
		||||
                .$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
 | 
			
		||||
      const name = await page
 | 
			
		||||
        .$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
 | 
			
		||||
            console.log(`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
 | 
			
		||||
      await page
 | 
			
		||||
        .waitForSelector(
 | 
			
		||||
          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
			
		||||
          { timeout: 5000 }
 | 
			
		||||
        )
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
      const current_price = await page
 | 
			
		||||
        .$eval(
 | 
			
		||||
          "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
 | 
			
		||||
          (el) => el.innerText
 | 
			
		||||
        )
 | 
			
		||||
        .catch(() => null);
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        `📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      const data = removeFalsyValues(
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			@ -179,7 +244,7 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
          name,
 | 
			
		||||
          current_price: current_price ? extractNumber(current_price) : null,
 | 
			
		||||
        },
 | 
			
		||||
                ['close_time'],
 | 
			
		||||
        ["close_time"]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      this.handleUpdateBid(data);
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +267,9 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      const { close_time, ...isCloseProduct } = await this.isCloseProduct();
 | 
			
		||||
      if (isCloseProduct.result) {
 | 
			
		||||
                console.log(`❌ [${this.id}] The product is closed, cannot place a bid.`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `❌ [${this.id}] The product is closed, cannot place a bid.`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -213,18 +280,24 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      const { result, bid_price } = await this.validate({ page, price_value });
 | 
			
		||||
      if (!result) {
 | 
			
		||||
                console.log(`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
      if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
 | 
			
		||||
                console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (price_value != bid_price) {
 | 
			
		||||
                console.log(`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`
 | 
			
		||||
        );
 | 
			
		||||
        await this.handleWritePrice(page, bid_price);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -232,14 +305,18 @@ export class GraysProductBid extends ProductBid {
 | 
			
		|||
      const resultPlaceBid = await this.placeBid(page);
 | 
			
		||||
      if (!resultPlaceBid) {
 | 
			
		||||
        console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
 | 
			
		||||
                await takeSnapshot(page, this, 'place-bid-action');
 | 
			
		||||
        await takeSnapshot(page, this, "place-bid-action");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            console.log(`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`
 | 
			
		||||
      );
 | 
			
		||||
      await this.handleReturnProductPage(page);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error(`🚨 [${this.id}] Error navigating the page: ${error.message}`);
 | 
			
		||||
      console.error(
 | 
			
		||||
        `🚨 [${this.id}] Error navigating the page: ${error.message}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import configs from '../../system/config.js';
 | 
			
		||||
import { getPathProfile, safeClosePage } from '../../system/utils.js';
 | 
			
		||||
import { ApiBid } from '../api-bid.js';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { updateStatusByPrice } from '../../system/apis/bid.js';
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import { getPathProfile, safeClosePage } from "../../system/utils.js";
 | 
			
		||||
import { ApiBid } from "../api-bid.js";
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
import { updateStatusByPrice } from "../../system/apis/bid.js";
 | 
			
		||||
 | 
			
		||||
export class LangtonsApiBid extends ApiBid {
 | 
			
		||||
  reloadInterval = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -16,7 +16,11 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
      // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
			
		||||
      const timeout = setTimeout(() => {
 | 
			
		||||
        global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
			
		||||
                rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`));
 | 
			
		||||
        rej(
 | 
			
		||||
          new Error(
 | 
			
		||||
            `[${this.id}] Timeout: No verification code received within 1 minute.`
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      }, 120 * 1000); // 120 giây
 | 
			
		||||
 | 
			
		||||
      global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -27,13 +31,16 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    async isLogin() {
 | 
			
		||||
  isLogin = async () => {
 | 
			
		||||
    if (!this.page_context) return false;
 | 
			
		||||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
        return !(await this.page_context.$('input[name="loginEmail"]')) && fs.existsSync(filePath);
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      !(await this.page_context.$('input[name="loginEmail"]')) &&
 | 
			
		||||
      fs.existsSync(filePath)
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async handleLogin() {
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,10 +49,13 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
        await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
    await page.waitForNavigation({ waitUntil: "domcontentloaded" });
 | 
			
		||||
 | 
			
		||||
    // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
			
		||||
        if (!(await page.$('input[name="loginEmail"]')) && fs.existsSync(filePath)) {
 | 
			
		||||
    if (
 | 
			
		||||
      !(await page.$('input[name="loginEmail"]')) &&
 | 
			
		||||
      fs.existsSync(filePath)
 | 
			
		||||
    ) {
 | 
			
		||||
      console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -56,19 +66,27 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    const children = this.children.filter((item) => item.page_context);
 | 
			
		||||
        console.log(`🔍 [${this.id}] Found ${children.length} child pages to close.`);
 | 
			
		||||
    console.log(
 | 
			
		||||
      `🔍 [${this.id}] Found ${children.length} child pages to close.`
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (children.length > 0) {
 | 
			
		||||
      console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        children.map((item) => {
 | 
			
		||||
                    console.log(`➡ [${this.id}] Closing child page with context: ${item.page_context}`);
 | 
			
		||||
          console.log(
 | 
			
		||||
            `➡ [${this.id}] Closing child page with context: ${item.page_context}`
 | 
			
		||||
          );
 | 
			
		||||
          return safeClosePage(item);
 | 
			
		||||
                }),
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
            console.log(`➡ [${this.id}] Closing main page context: ${this.page_context}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `➡ [${this.id}] Closing main page context: ${this.page_context}`
 | 
			
		||||
      );
 | 
			
		||||
      await safeClosePage(this);
 | 
			
		||||
 | 
			
		||||
      await this.onCloseLogin(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`🔑 [${this.id}] Starting login process...`);
 | 
			
		||||
| 
						 | 
				
			
			@ -76,46 +94,65 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      // ⌨ Enter email
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
			
		||||
            await page.type('input[name="loginEmail"]', this.username, { delay: 100 });
 | 
			
		||||
      await page.type('input[name="loginEmail"]', this.username, {
 | 
			
		||||
        delay: 100,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // ⌨ Enter password
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering password...`);
 | 
			
		||||
            await page.type('input[name="loginPassword"]', this.password, { delay: 150 });
 | 
			
		||||
      await page.type('input[name="loginPassword"]', this.password, {
 | 
			
		||||
        delay: 150,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // ✅ Click the "Remember Me" checkbox
 | 
			
		||||
      console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
 | 
			
		||||
            await page.click('#rememberMe', { delay: 80 });
 | 
			
		||||
      await page.click("#rememberMe", { delay: 80 });
 | 
			
		||||
 | 
			
		||||
      // 🚀 Click the login button
 | 
			
		||||
      console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
			
		||||
            await page.click('#loginFormSubmitButton', { delay: 92 });
 | 
			
		||||
      await page.click("#loginFormSubmitButton", { delay: 92 });
 | 
			
		||||
 | 
			
		||||
      // ⏳ Wait for navigation after login
 | 
			
		||||
      console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
			
		||||
            await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
			
		||||
      await page.waitForNavigation({
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log(`🌍 [${this.id}] Current page after login:`, page.url());
 | 
			
		||||
 | 
			
		||||
      // 📢 Listen for verification code event
 | 
			
		||||
            console.log(`👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // ⏳ Wait for verification code from socket event
 | 
			
		||||
      const { name, code } = await this.waitVerifyData();
 | 
			
		||||
            console.log(`✅ [${this.id}] Verification code received:`, { name, code });
 | 
			
		||||
      console.log(`✅ [${this.id}] Verification code received:`, {
 | 
			
		||||
        name,
 | 
			
		||||
        code,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // ⌨ Enter verification code
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering verification code...`);
 | 
			
		||||
            await page.type('#code', code, { delay: 120 });
 | 
			
		||||
      await page.type("#code", code, { delay: 120 });
 | 
			
		||||
 | 
			
		||||
      // 🚀 Click the verification confirmation button
 | 
			
		||||
            console.log(`🔘 [${this.id}] Clicking the verification confirmation button`);
 | 
			
		||||
            await page.click('.btn.btn-block.btn-primary', { delay: 90 });
 | 
			
		||||
      console.log(
 | 
			
		||||
        `🔘 [${this.id}] Clicking the verification confirmation button`
 | 
			
		||||
      );
 | 
			
		||||
      await page.click(".btn.btn-block.btn-primary", { delay: 90 });
 | 
			
		||||
 | 
			
		||||
      // ⏳ Wait for navigation after verification
 | 
			
		||||
            console.log(`⏳ [${this.id}] Waiting for navigation after verification...`);
 | 
			
		||||
            await page.waitForNavigation({ timeout: 15000, waitUntil: 'domcontentloaded' });
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⏳ [${this.id}] Waiting for navigation after verification...`
 | 
			
		||||
      );
 | 
			
		||||
      await page.waitForNavigation({
 | 
			
		||||
        timeout: 15000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
      // 📂 Save session context to avoid re-login
 | 
			
		||||
      await this.saveContext();
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +161,10 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
      // await page.goto(this.url);
 | 
			
		||||
      console.log(`✅ [${this.id}] Navigation successful!`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error(`❌ [${this.id}] Error during login process:`, error.message);
 | 
			
		||||
      console.error(
 | 
			
		||||
        `❌ [${this.id}] Error during login process:`,
 | 
			
		||||
        error.message
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      global.IS_CLEANING = true;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -132,10 +172,14 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
 | 
			
		||||
  async getWonList() {
 | 
			
		||||
    try {
 | 
			
		||||
            await page.waitForSelector('.row.account-product-list', { timeout: 30000 });
 | 
			
		||||
      await page.waitForSelector(".row.account-product-list", {
 | 
			
		||||
        timeout: 30000,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const items = await page.evaluate(() => {
 | 
			
		||||
                return Array.from(document.querySelectorAll('.row.account-product-list')).map((item) => item.getAttribute('data-lotid') || null);
 | 
			
		||||
        return Array.from(
 | 
			
		||||
          document.querySelectorAll(".row.account-product-list")
 | 
			
		||||
        ).map((item) => item.getAttribute("data-lotid") || null);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return _.compact(items);
 | 
			
		||||
| 
						 | 
				
			
			@ -158,18 +202,29 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
 | 
			
		||||
        const result = _.filter(this.children, (item) => _.includes(items, item.lot_id));
 | 
			
		||||
        console.log(`✅ [${this.id}] ${result.length} items need to be updated:`, result);
 | 
			
		||||
    const result = _.filter(this.children, (item) =>
 | 
			
		||||
      _.includes(items, item.lot_id)
 | 
			
		||||
    );
 | 
			
		||||
    console.log(
 | 
			
		||||
      `✅ [${this.id}] ${result.length} items need to be updated:`,
 | 
			
		||||
      result
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
 | 
			
		||||
        const responses = await Promise.allSettled(result.map((i) => updateStatusByPrice(i.id, i.current_price)));
 | 
			
		||||
    const responses = await Promise.allSettled(
 | 
			
		||||
      result.map((i) => updateStatusByPrice(i.id, i.current_price))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Log kết quả của mỗi request
 | 
			
		||||
    responses.forEach((response, index) => {
 | 
			
		||||
            if (response.status === 'fulfilled') {
 | 
			
		||||
      if (response.status === "fulfilled") {
 | 
			
		||||
        console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
 | 
			
		||||
      } else {
 | 
			
		||||
                console.error(`❌ [${this.id}] Update failed:`, result[index], response.reason);
 | 
			
		||||
        console.error(
 | 
			
		||||
          `❌ [${this.id}] Update failed:`,
 | 
			
		||||
          result[index],
 | 
			
		||||
          response.reason
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +236,7 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
    try {
 | 
			
		||||
      const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
            page.on('response', async (response) => {
 | 
			
		||||
      page.on("response", async (response) => {
 | 
			
		||||
        const request = response.request();
 | 
			
		||||
        if (request.redirectChain().length > 0) {
 | 
			
		||||
          if (response.url().includes(configs.WEB_CONFIGS.LANGTONS.LOGIN_URL)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -190,14 +245,16 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      // Set userAgent
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.log('Error [action]: ', error.message);
 | 
			
		||||
      console.log("Error [action]: ", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -211,12 +268,14 @@ export class LangtonsApiBid extends ApiBid {
 | 
			
		|||
      try {
 | 
			
		||||
        if (this.page_context && !this.page_context.isClosed()) {
 | 
			
		||||
          console.log(`🔄 [${this.id}] Reloading page...`);
 | 
			
		||||
                    await this.page_context.reload({ waitUntil: 'networkidle2' });
 | 
			
		||||
          await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
			
		||||
          console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
			
		||||
 | 
			
		||||
          // this.handleUpdateWonItem();
 | 
			
		||||
        } else {
 | 
			
		||||
                    console.log(`❌ [${this.id}] Page context is closed. Stopping reload.`);
 | 
			
		||||
          console.log(
 | 
			
		||||
            `❌ [${this.id}] Page context is closed. Stopping reload.`
 | 
			
		||||
          );
 | 
			
		||||
          clearInterval(this.reloadInterval);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,16 @@
 | 
			
		|||
import _ from 'lodash';
 | 
			
		||||
import { outBid, pushPrice, updateBid } from '../../system/apis/bid.js';
 | 
			
		||||
import { sendMessage } from '../../system/apis/notification.js';
 | 
			
		||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
 | 
			
		||||
import configs from '../../system/config.js';
 | 
			
		||||
import CONSTANTS from '../../system/constants.js';
 | 
			
		||||
import { convertAETtoUTC, isTimeReached, removeFalsyValues, takeSnapshot } from '../../system/utils.js';
 | 
			
		||||
import { ProductBid } from '../product-bid.js';
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
 | 
			
		||||
import { sendMessage } from "../../system/apis/notification.js";
 | 
			
		||||
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import CONSTANTS from "../../system/constants.js";
 | 
			
		||||
import {
 | 
			
		||||
  convertAETtoUTC,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  removeFalsyValues,
 | 
			
		||||
  takeSnapshot,
 | 
			
		||||
} from "../../system/utils.js";
 | 
			
		||||
import { ProductBid } from "../product-bid.js";
 | 
			
		||||
 | 
			
		||||
export class LangtonsProductBid extends ProductBid {
 | 
			
		||||
  constructor({ ...prev }) {
 | 
			
		||||
| 
						 | 
				
			
			@ -18,9 +23,11 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
      // Kiểm tra xem có context của trang web không, nếu không thì trả về null
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('.site-timezone', { timeout: 2000 });
 | 
			
		||||
      await this.page_context.waitForSelector(".site-timezone", {
 | 
			
		||||
        timeout: 2000,
 | 
			
		||||
      });
 | 
			
		||||
      const time = await this.page_context.evaluate(() => {
 | 
			
		||||
                const el = document.querySelector('.site-timezone');
 | 
			
		||||
        const el = document.querySelector(".site-timezone");
 | 
			
		||||
        return el ? el.innerText : null;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,32 +49,57 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
    return new Promise((resolve) => {
 | 
			
		||||
      const onResponse = async (response) => {
 | 
			
		||||
        try {
 | 
			
		||||
                    if (!response || !response.request().url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
 | 
			
		||||
          if (
 | 
			
		||||
            !response ||
 | 
			
		||||
            !response
 | 
			
		||||
              .request()
 | 
			
		||||
              .url()
 | 
			
		||||
              .includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
 | 
			
		||||
          ) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          clearTimeout(timer); // Hủy timeout nếu có phản hồi
 | 
			
		||||
                    this.page_context.off('response', onResponse); // Gỡ bỏ listener
 | 
			
		||||
          this.page_context.off("response", onResponse); // Gỡ bỏ listener
 | 
			
		||||
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          resolve(data);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
                    console.error(`❌ [${this.id}] Error while parsing response:`, error?.message);
 | 
			
		||||
          console.error(
 | 
			
		||||
            `❌ [${this.id}] Error while parsing response:`,
 | 
			
		||||
            error?.message
 | 
			
		||||
          );
 | 
			
		||||
          resolve(null);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const timer = setTimeout(async () => {
 | 
			
		||||
                console.log(`⏳ [${this.id}] Timeout: No response received within ${timeout / 1000}s`);
 | 
			
		||||
                this.page_context.off('response', onResponse); // Gỡ bỏ listener khi timeout
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⏳ [${this.id}] Timeout: No response received within ${
 | 
			
		||||
            timeout / 1000
 | 
			
		||||
          }s`
 | 
			
		||||
        );
 | 
			
		||||
        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);
 | 
			
		||||
      }, timeout);
 | 
			
		||||
 | 
			
		||||
            this.page_context.on('response', onResponse);
 | 
			
		||||
      this.page_context.on("response", onResponse);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -75,10 +107,12 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('.product-name', { timeout: 3000 });
 | 
			
		||||
      await this.page_context.waitForSelector(".product-name", {
 | 
			
		||||
        timeout: 3000,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return await this.page_context.evaluate(() => {
 | 
			
		||||
                const el = document.querySelector('.product-name');
 | 
			
		||||
        const el = document.querySelector(".product-name");
 | 
			
		||||
        return el ? el.innerText : null;
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -86,8 +120,22 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, model }) {
 | 
			
		||||
        const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, model });
 | 
			
		||||
  async handleUpdateBid({
 | 
			
		||||
    lot_id,
 | 
			
		||||
    close_time,
 | 
			
		||||
    name,
 | 
			
		||||
    current_price,
 | 
			
		||||
    reserve_price,
 | 
			
		||||
    model,
 | 
			
		||||
  }) {
 | 
			
		||||
    const response = await updateBid(this.id, {
 | 
			
		||||
      lot_id,
 | 
			
		||||
      close_time,
 | 
			
		||||
      name,
 | 
			
		||||
      current_price,
 | 
			
		||||
      reserve_price: Number(reserve_price) || 0,
 | 
			
		||||
      model,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (response) {
 | 
			
		||||
      this.lot_id = response.lot_id;
 | 
			
		||||
| 
						 | 
				
			
			@ -130,7 +178,7 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
        name,
 | 
			
		||||
      },
 | 
			
		||||
      // [],
 | 
			
		||||
            ['close_time'],
 | 
			
		||||
      ["close_time"]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    console.log(`🚀 [${this.id}] Processed data ready for update`);
 | 
			
		||||
| 
						 | 
				
			
			@ -138,7 +186,7 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
    // 📌 Gửi dữ liệu cập nhật lên hệ thống
 | 
			
		||||
    await this.handleUpdateBid(data);
 | 
			
		||||
 | 
			
		||||
        console.log('✅ Update successful!');
 | 
			
		||||
    console.log("✅ Update successful!");
 | 
			
		||||
 | 
			
		||||
    return { ...response, name, close_time };
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -147,10 +195,15 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('.btn.btn-block.btn-primary.error.continue-shopping', { timeout: 3000 });
 | 
			
		||||
      await this.page_context.waitForSelector(
 | 
			
		||||
        ".btn.btn-block.btn-primary.error.continue-shopping",
 | 
			
		||||
        { timeout: 3000 }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return await this.page_context.evaluate(() => {
 | 
			
		||||
                const el = document.querySelector('.btn.btn-block.btn-primary.error.continue-shopping');
 | 
			
		||||
        const el = document.querySelector(
 | 
			
		||||
          ".btn.btn-block.btn-primary.error.continue-shopping"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return el;
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -161,7 +214,9 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
  async handlePlaceBid() {
 | 
			
		||||
    if (!this.page_context) {
 | 
			
		||||
            console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⚠️ [${this.id}] No page context found, aborting bid process.`
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +232,9 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      const continueShopBtn = await this.getContinueShopButton();
 | 
			
		||||
      if (continueShopBtn) {
 | 
			
		||||
                console.log(`⚠️ [${this.id}] Outbid detected, calling outBid function.`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⚠️ [${this.id}] Outbid detected, calling outBid function.`
 | 
			
		||||
        );
 | 
			
		||||
        await outBid(this.id);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +247,11 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      // Kiểm tra thời gian bid
 | 
			
		||||
      if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
 | 
			
		||||
                console.log(`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${this.name || 'None'}`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
 | 
			
		||||
            this.name || "None"
 | 
			
		||||
          }`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -198,40 +259,65 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
      const response = await this.waitForApiResponse();
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
 | 
			
		||||
            if (!response || (response?.lotData?.myBid && response.lotData.myBid == this.max_price) || response?.lotData?.minimumBid > this.max_price) {
 | 
			
		||||
                console.log(`⚠️ [${this.id}] No response or myBid equals max_price:`, response); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
 | 
			
		||||
      if (
 | 
			
		||||
        !response ||
 | 
			
		||||
        (response?.lotData?.myBid &&
 | 
			
		||||
          response.lotData.myBid == this.max_price) ||
 | 
			
		||||
        response?.lotData?.minimumBid > this.max_price
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⚠️ [${this.id}] No response or myBid equals max_price:`,
 | 
			
		||||
          response
 | 
			
		||||
        ); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
 | 
			
		||||
        return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra nếu dữ liệu trong response có tồn tại và trạng thái đấu giá (bidStatus) không phải là 'None'
 | 
			
		||||
            if (response.lotData && response.lotData?.bidStatus !== 'None' && this.max_price == response?.lotData.myBid) {
 | 
			
		||||
                console.log(`✔️ [${this.id}] Bid status is not 'None'. Current bid status:`, response.lotData?.bidStatus); // Ghi log nếu trạng thái đấu giá không phải 'None'
 | 
			
		||||
      if (
 | 
			
		||||
        response.lotData &&
 | 
			
		||||
        response.lotData?.bidStatus !== "None" &&
 | 
			
		||||
        this.max_price == response?.lotData.myBid
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `✔️ [${this.id}] Bid status is not 'None'. Current bid status:`,
 | 
			
		||||
          response.lotData?.bidStatus
 | 
			
		||||
        ); // Ghi log nếu trạng thái đấu giá không phải 'None'
 | 
			
		||||
        return; // Nếu trạng thái đấu giá không phải là 'None', dừng hàm
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
      console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
			
		||||
 | 
			
		||||
            if (bidHistoriesItem && bidHistoriesItem?.price === this.current_price && this.max_price == response?.lotData.myBid) {
 | 
			
		||||
                console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
 | 
			
		||||
      if (
 | 
			
		||||
        bidHistoriesItem &&
 | 
			
		||||
        bidHistoriesItem?.price === this.current_price &&
 | 
			
		||||
        this.max_price == response?.lotData.myBid
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            console.log(`💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // 📌 Làm rỗng ô input trước khi nhập giá đấu
 | 
			
		||||
      await page.evaluate(() => {
 | 
			
		||||
                document.querySelector('#place-bid').value = '';
 | 
			
		||||
        document.querySelector("#place-bid").value = "";
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log(`📝 [${this.id}] Cleared bid input field.`);
 | 
			
		||||
 | 
			
		||||
      // 📌 Nhập giá đấu vào ô input
 | 
			
		||||
            await page.type('#place-bid', String(this.max_price), { delay: 800 });
 | 
			
		||||
      await page.type("#place-bid", String(this.max_price), { delay: 800 });
 | 
			
		||||
      console.log(`✅ [${this.id}] Entered bid amount: ${this.max_price}`);
 | 
			
		||||
 | 
			
		||||
      // 📌 Lấy giá trị thực tế từ ô input sau khi nhập
 | 
			
		||||
            const bidValue = await page.evaluate(() => document.querySelector('#place-bid').value);
 | 
			
		||||
      const bidValue = await page.evaluate(
 | 
			
		||||
        () => document.querySelector("#place-bid").value
 | 
			
		||||
      );
 | 
			
		||||
      console.log(`🔍 Entered bid value: ${bidValue}`);
 | 
			
		||||
 | 
			
		||||
      // 📌 Kiểm tra nếu giá trị nhập vào không khớp với giá trị mong muốn
 | 
			
		||||
| 
						 | 
				
			
			@ -241,13 +327,19 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
      }
 | 
			
		||||
 | 
			
		||||
      // 📌 Nhấn nút "Place Bid"
 | 
			
		||||
            await page.click('.place-bid-submit .btn.btn-primary.btn-block.place-bid-btn', { delay: 5000 });
 | 
			
		||||
      await page.click(
 | 
			
		||||
        ".place-bid-submit .btn.btn-primary.btn-block.place-bid-btn",
 | 
			
		||||
        { delay: 5000 }
 | 
			
		||||
      );
 | 
			
		||||
      console.log(`🖱️ [${this.id}] Clicked "Place Bid" button.`);
 | 
			
		||||
 | 
			
		||||
      console.log(`📩 [${this.id}] Bid submitted, waiting for navigation...`);
 | 
			
		||||
 | 
			
		||||
      // 📌 Chờ trang load lại để cập nhật trạng thái đấu giá
 | 
			
		||||
            await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
 | 
			
		||||
      await page.waitForNavigation({
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +349,12 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
      // 📌 Kiểm tra trạng thái đấu giá từ API
 | 
			
		||||
      if (lotData?.myBid == this.max_price) {
 | 
			
		||||
        console.log(`📸 [${this.id}] Taking bid success snapshot...`);
 | 
			
		||||
                await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
 | 
			
		||||
        await takeSnapshot(
 | 
			
		||||
          page,
 | 
			
		||||
          this,
 | 
			
		||||
          "bid-success",
 | 
			
		||||
          CONSTANTS.TYPE_IMAGE.SUCCESS
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        sendMessage(this);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -265,7 +362,9 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            console.log(`⚠️ [${this.id}] Bid action completed, but status is still "None".`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⚠️ [${this.id}] Bid action completed, but status is still "None".`
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
 | 
			
		||||
    } finally {
 | 
			
		||||
| 
						 | 
				
			
			@ -299,14 +398,16 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
      console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
 | 
			
		||||
 | 
			
		||||
      console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      console.log(`🛠️ [${this.id}] Setting custom user agent...`);
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(`🎯 [${this.id}] Listening for API responses...`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -337,7 +438,10 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      const onResponse = async (response) => {
 | 
			
		||||
        const url = response?.request()?.url();
 | 
			
		||||
                if (!url || !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
 | 
			
		||||
        if (
 | 
			
		||||
          !url ||
 | 
			
		||||
          !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
 | 
			
		||||
        ) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -346,22 +450,33 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
          console.log(`📜 [${this.id}] Received lotData:`, lotData);
 | 
			
		||||
 | 
			
		||||
          if (!lotData || lotData.lotId !== this.lot_id) {
 | 
			
		||||
                        console.log(`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`);
 | 
			
		||||
                        await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!this.page_context.isClosed()) {
 | 
			
		||||
              await this.page_context.reload({ waitUntil: "networkidle0" });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            console.log(`🔁 [${this.id}] Reload page in gotoLink`);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          console.log(`🔍 [${this.id}] Checking bid status...`);
 | 
			
		||||
 | 
			
		||||
                    if (['Outbid'].includes(lotData?.bidStatus)) {
 | 
			
		||||
                        console.log(`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`);
 | 
			
		||||
          if (["Outbid"].includes(lotData?.bidStatus)) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            this.handleCreateLogsOnServer([{ lotData, ...prev }]);
 | 
			
		||||
                    } else if (['Winning'].includes(lotData?.bidStatus)) {
 | 
			
		||||
                        const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
          } else if (["Winning"].includes(lotData?.bidStatus)) {
 | 
			
		||||
            const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
 | 
			
		||||
                        if (!bidHistoriesItem || bidHistoriesItem?.price != lotData?.currentMaxBid) {
 | 
			
		||||
            if (
 | 
			
		||||
              !bidHistoriesItem ||
 | 
			
		||||
              bidHistoriesItem?.price != lotData?.currentMaxBid
 | 
			
		||||
            ) {
 | 
			
		||||
              pushPrice({
 | 
			
		||||
                bid_id: this.id,
 | 
			
		||||
                price: lotData?.currentMaxBid,
 | 
			
		||||
| 
						 | 
				
			
			@ -369,7 +484,11 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
                    if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
 | 
			
		||||
          if (
 | 
			
		||||
            lotData.myBid &&
 | 
			
		||||
            this.max_price &&
 | 
			
		||||
            this.max_price != lotData.myBid
 | 
			
		||||
          ) {
 | 
			
		||||
            this.handlePlaceBid();
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -378,10 +497,10 @@ export class LangtonsProductBid extends ProductBid {
 | 
			
		|||
      };
 | 
			
		||||
 | 
			
		||||
      console.log(`🔄 [${this.id}] Removing previous response listeners...`);
 | 
			
		||||
            this.page_context.off('response', onResponse);
 | 
			
		||||
      this.page_context.off("response", onResponse);
 | 
			
		||||
 | 
			
		||||
      console.log(`📡 [${this.id}] Attaching new response listener...`);
 | 
			
		||||
            this.page_context.on('response', onResponse);
 | 
			
		||||
      this.page_context.on("response", onResponse);
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Navigation setup complete.`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,34 @@
 | 
			
		|||
import _ from 'lodash';
 | 
			
		||||
import { pushPrice, updateBid } from '../../system/apis/bid.js';
 | 
			
		||||
import { sendMessage } from '../../system/apis/notification.js';
 | 
			
		||||
import configs from '../../system/config.js';
 | 
			
		||||
import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
 | 
			
		||||
import { ProductBid } from '../product-bid.js';
 | 
			
		||||
import _ from "lodash";
 | 
			
		||||
import { pushPrice, updateBid } from "../../system/apis/bid.js";
 | 
			
		||||
import { sendMessage } from "../../system/apis/notification.js";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import {
 | 
			
		||||
  delay,
 | 
			
		||||
  extractPriceNumber,
 | 
			
		||||
  isTimeReached,
 | 
			
		||||
  removeFalsyValues,
 | 
			
		||||
} from "../../system/utils.js";
 | 
			
		||||
import { ProductBid } from "../product-bid.js";
 | 
			
		||||
 | 
			
		||||
export class LawsonsProductBid extends ProductBid {
 | 
			
		||||
  constructor({ ...prev }) {
 | 
			
		||||
    super(prev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
 | 
			
		||||
        const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
 | 
			
		||||
  async handleUpdateBid({
 | 
			
		||||
    lot_id,
 | 
			
		||||
    close_time,
 | 
			
		||||
    name,
 | 
			
		||||
    current_price,
 | 
			
		||||
    reserve_price,
 | 
			
		||||
  }) {
 | 
			
		||||
    const response = await updateBid(this.id, {
 | 
			
		||||
      lot_id,
 | 
			
		||||
      close_time,
 | 
			
		||||
      name,
 | 
			
		||||
      current_price,
 | 
			
		||||
      reserve_price: Number(reserve_price) || 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (response) {
 | 
			
		||||
      this.lot_id = response.lot_id;
 | 
			
		||||
| 
						 | 
				
			
			@ -24,9 +41,14 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      if (!this.page_context) return null;
 | 
			
		||||
 | 
			
		||||
            await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
 | 
			
		||||
      await this.page_context.waitForSelector(
 | 
			
		||||
        ".select-dropdown-value.text-truncate",
 | 
			
		||||
        { timeout: 4000 }
 | 
			
		||||
      );
 | 
			
		||||
      const price = await this.page_context.evaluate(() => {
 | 
			
		||||
                const el = document.querySelector('.select-dropdown-value.text-truncate');
 | 
			
		||||
        const el = document.querySelector(
 | 
			
		||||
          ".select-dropdown-value.text-truncate"
 | 
			
		||||
        );
 | 
			
		||||
        return el ? el.innerText : null;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +85,7 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
          name: result?.itemView?.title || null,
 | 
			
		||||
        },
 | 
			
		||||
        // [],
 | 
			
		||||
                ['close_time'],
 | 
			
		||||
        ["close_time"]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(`🚀 [${this.id}] Processed data ready for update`);
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +93,7 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
      // 📌 Gửi dữ liệu cập nhật lên hệ thống
 | 
			
		||||
      await this.handleUpdateBid(data);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.log('Error Update', error.message);
 | 
			
		||||
      console.log("Error Update", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,8 +102,8 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
    return await this.page_context.evaluate(async (url) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch(url, {
 | 
			
		||||
                    method: 'GET',
 | 
			
		||||
                    headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
          method: "GET",
 | 
			
		||||
          headers: { "Content-Type": "application/json" },
 | 
			
		||||
        });
 | 
			
		||||
        return await res.json();
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,9 +126,9 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
        const result = await this.page_context.evaluate(
 | 
			
		||||
          async (bidAmount, lotRef, url) => {
 | 
			
		||||
            const response = await fetch(url, {
 | 
			
		||||
                            method: 'POST',
 | 
			
		||||
              method: "POST",
 | 
			
		||||
              headers: {
 | 
			
		||||
                                'Content-Type': 'application/json',
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
              },
 | 
			
		||||
              body: JSON.stringify({
 | 
			
		||||
                bidAmount,
 | 
			
		||||
| 
						 | 
				
			
			@ -123,10 +145,10 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
          },
 | 
			
		||||
          this.max_price,
 | 
			
		||||
          this.model,
 | 
			
		||||
                    configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT,
 | 
			
		||||
          configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
                console.log('🧾 API Bid Result:', {
 | 
			
		||||
        console.log("🧾 API Bid Result:", {
 | 
			
		||||
          bid_amount: this.max_price,
 | 
			
		||||
          result,
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +166,9 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
  async handlePlaceBid() {
 | 
			
		||||
    // Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
 | 
			
		||||
    if (!this.page_context) {
 | 
			
		||||
            console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⚠️ [${this.id}] No page context found, aborting bid process.`
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +192,11 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
      // Kiểm tra thời gian đấu giá
 | 
			
		||||
      if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
 | 
			
		||||
                console.log(`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${this.name || 'None'}`);
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
 | 
			
		||||
            this.name || "None"
 | 
			
		||||
          }`
 | 
			
		||||
        );
 | 
			
		||||
        return; // Nếu chưa đến giờ đấu giá thì bỏ qua
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -187,17 +215,27 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
        reservePrice > this.max_price + this.plus_price;
 | 
			
		||||
 | 
			
		||||
      if (shouldStop) {
 | 
			
		||||
                console.log(`⚠️ [${this.id}] Stop bidding:`, { reservePrice, currentBidAmount: response?.currentBidAmount, maxBidAmount: response?.maxBidAmount });
 | 
			
		||||
        console.log(`⚠️ [${this.id}] Stop bidding:`, {
 | 
			
		||||
          reservePrice,
 | 
			
		||||
          currentBidAmount: response?.currentBidAmount,
 | 
			
		||||
          maxBidAmount: response?.maxBidAmount,
 | 
			
		||||
        });
 | 
			
		||||
        return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Tìm bid history lớn nhất từ các lịch sử đấu giá của item
 | 
			
		||||
            const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
      console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua
 | 
			
		||||
            if (bidHistoriesItem && bidHistoriesItem?.price == this.current_price && this.max_price + this.plus_price == response?.maxBidAmount) {
 | 
			
		||||
                console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`);
 | 
			
		||||
      if (
 | 
			
		||||
        bidHistoriesItem &&
 | 
			
		||||
        bidHistoriesItem?.price == this.current_price &&
 | 
			
		||||
        this.max_price + this.plus_price == response?.maxBidAmount
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -206,12 +244,16 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
            console.log(`===============Start call to submit [${this.id}] ================`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `===============Start call to submit [${this.id}] ================`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
            await delay(20000);
 | 
			
		||||
      await delay(200);
 | 
			
		||||
 | 
			
		||||
      // Nếu chưa bid, thực hiện đặt giá
 | 
			
		||||
            console.log(`💰 [${this.id}] Placing a bid with amount: ${this.max_price}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `💰 [${this.id}] Placing a bid with amount: ${this.max_price}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Gửi bid qua API và nhận kết quả
 | 
			
		||||
      const result = await this.submitBid();
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +266,7 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
      // Gửi thông báo đã đấu giá thành công
 | 
			
		||||
      sendMessage(this);
 | 
			
		||||
 | 
			
		||||
            await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
			
		||||
      await this.page_context.reload({ waitUntil: "networkidle0" });
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Bid placed successfully!`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -244,9 +286,14 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model);
 | 
			
		||||
        const detailUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_PRODUCT(this.model);
 | 
			
		||||
    const detailUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_PRODUCT(
 | 
			
		||||
      this.model
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
        const [info, detailData] = await Promise.all([this.fetchFromPage(infoUrl), this.fetchFromPage(detailUrl)]);
 | 
			
		||||
    const [info, detailData] = await Promise.all([
 | 
			
		||||
      this.fetchFromPage(infoUrl),
 | 
			
		||||
      this.fetchFromPage(detailUrl),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return { ...info, ...detailData };
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +304,10 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
    try {
 | 
			
		||||
      const onResponse = async (response) => {
 | 
			
		||||
        const url = response?.request()?.url();
 | 
			
		||||
                if (!url || !url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))) {
 | 
			
		||||
        if (
 | 
			
		||||
          !url ||
 | 
			
		||||
          !url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))
 | 
			
		||||
        ) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -270,21 +320,33 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
          const { maxBidAmount, currentBidAmount, isOutBid } = result;
 | 
			
		||||
 | 
			
		||||
                    console.log(`📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`);
 | 
			
		||||
          console.log(
 | 
			
		||||
            `📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
 | 
			
		||||
          const reversePrice = await this.getReversePrice();
 | 
			
		||||
          console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
 | 
			
		||||
 | 
			
		||||
          // Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
 | 
			
		||||
                    const bidHistoriesItem = _.maxBy(this.histories, 'price');
 | 
			
		||||
                    console.log(`📈 [${this.id}] Highest local bid: ${bidHistoriesItem?.price ?? 'N/A'}`);
 | 
			
		||||
          const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
          console.log(
 | 
			
		||||
            `📈 [${this.id}] Highest local bid: ${
 | 
			
		||||
              bidHistoriesItem?.price ?? "N/A"
 | 
			
		||||
            }`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (!this.close_time || !this.lot_id || !this.current_price) return;
 | 
			
		||||
 | 
			
		||||
          // Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
 | 
			
		||||
                    if ((!bidHistoriesItem && maxBidAmount) || (bidHistoriesItem?.price != currentBidAmount && currentBidAmount == maxBidAmount)) {
 | 
			
		||||
                        console.log(`🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`);
 | 
			
		||||
          if (
 | 
			
		||||
            (!bidHistoriesItem && maxBidAmount) ||
 | 
			
		||||
            (bidHistoriesItem?.price != currentBidAmount &&
 | 
			
		||||
              currentBidAmount == maxBidAmount)
 | 
			
		||||
          ) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`
 | 
			
		||||
            );
 | 
			
		||||
            pushPrice({
 | 
			
		||||
              bid_id: this.id,
 | 
			
		||||
              price: currentBidAmount,
 | 
			
		||||
| 
						 | 
				
			
			@ -292,15 +354,25 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
          }
 | 
			
		||||
 | 
			
		||||
          // Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp
 | 
			
		||||
                    if (reversePrice <= this.max_price + this.plus_price && isOutBid && currentBidAmount <= this.max_price + this.plus_price && this.max_price != maxBidAmount) {
 | 
			
		||||
                        console.log(`⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`);
 | 
			
		||||
          if (
 | 
			
		||||
            reversePrice <= this.max_price + this.plus_price &&
 | 
			
		||||
            isOutBid &&
 | 
			
		||||
            currentBidAmount <= this.max_price + this.plus_price &&
 | 
			
		||||
            this.max_price != maxBidAmount
 | 
			
		||||
          ) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`
 | 
			
		||||
            );
 | 
			
		||||
            await this.handlePlaceBid();
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
                    if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
 | 
			
		||||
                        await this.page_context.reload({ waitUntil: 'networkidle0' });
 | 
			
		||||
          if (
 | 
			
		||||
            new Date(this.updated_at).getTime() > Date.now() - 120 * 1000 &&
 | 
			
		||||
            !this.page_context.isClosed()
 | 
			
		||||
          ) {
 | 
			
		||||
            await this.page_context.reload({ waitUntil: "networkidle0" });
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error(`🚨 [${this.id}] Error parsing API response:`, error);
 | 
			
		||||
| 
						 | 
				
			
			@ -308,10 +380,10 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
      };
 | 
			
		||||
 | 
			
		||||
      console.log(`🔄 [${this.id}] Removing previous response listeners...`);
 | 
			
		||||
            this.page_context.off('response', onResponse);
 | 
			
		||||
      this.page_context.off("response", onResponse);
 | 
			
		||||
 | 
			
		||||
      console.log(`📡 [${this.id}] Attaching new response listener...`);
 | 
			
		||||
            this.page_context.on('response', onResponse);
 | 
			
		||||
      this.page_context.on("response", onResponse);
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Navigation setup complete.`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -331,14 +403,16 @@ export class LawsonsProductBid extends ProductBid {
 | 
			
		|||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
      console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
 | 
			
		||||
 | 
			
		||||
      console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      console.log(`🛠️ [${this.id}] Setting custom user agent...`);
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(`🎯 [${this.id}] Listening for API responses...`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,151 @@
 | 
			
		|||
import fs from "fs";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import { delay, getPathProfile, safeClosePage } from "../../system/utils.js";
 | 
			
		||||
import { ApiBid } from "../api-bid.js";
 | 
			
		||||
 | 
			
		||||
export class PicklesApiBid extends ApiBid {
 | 
			
		||||
  reloadInterval = null;
 | 
			
		||||
  constructor({ ...prev }) {
 | 
			
		||||
    super(prev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  action = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
      page.on("response", async (response) => {
 | 
			
		||||
        const request = response.request();
 | 
			
		||||
        if (request.redirectChain().length > 0) {
 | 
			
		||||
          if (response.url().includes(configs.WEB_CONFIGS.PICKLES.LOGIN_URL)) {
 | 
			
		||||
            await this.handleLogin();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      // Set userAgent
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log("Error [action]: ", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  isLogin = async () => {
 | 
			
		||||
    if (!this.page_context) return false;
 | 
			
		||||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      !(await this.page_context.$('[name="_58_login"]')) &&
 | 
			
		||||
      fs.existsSync(filePath)
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async handleLogin() {
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
    global.IS_CLEANING = false;
 | 
			
		||||
 | 
			
		||||
    const filePath = getPathProfile(this.origin_url);
 | 
			
		||||
 | 
			
		||||
    await page.waitForNavigation({ waitUntil: "domcontentloaded" });
 | 
			
		||||
 | 
			
		||||
    // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
			
		||||
    if (!(await page.$('[name="_58_login"]')) && fs.existsSync(filePath)) {
 | 
			
		||||
      console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fs.existsSync(filePath)) {
 | 
			
		||||
      console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
			
		||||
      fs.unlinkSync(filePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const children = this.children.filter((item) => item?.page_context);
 | 
			
		||||
    console.log(
 | 
			
		||||
      `🔍 [${this.id}] Found ${children.length} child pages to close.`
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (children.length > 0) {
 | 
			
		||||
      console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        children.map((item) => {
 | 
			
		||||
          console.log(
 | 
			
		||||
            `➡ [${this.id}] Closing child page with context: ${item?.page_context}`
 | 
			
		||||
          );
 | 
			
		||||
          return safeClosePage(item);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        `➡ [${this.id}] Closing main page context: ${this.page_context}`
 | 
			
		||||
      );
 | 
			
		||||
      await safeClosePage(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`🔑 [${this.id}] Starting login process...`);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // ⌨ Enter email
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
			
		||||
      await page.type('[name="_58_login"]', this.username, { delay: 100 });
 | 
			
		||||
 | 
			
		||||
      // ⌨ Enter password
 | 
			
		||||
      console.log(`✍ [${this.id}] Entering password...`);
 | 
			
		||||
      await page.type('[name="_58_password"]', this.password, { delay: 150 });
 | 
			
		||||
 | 
			
		||||
      // 🚀 Click the login button
 | 
			
		||||
      console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
			
		||||
      await page.click("#sign-in-btn", { delay: 92 });
 | 
			
		||||
 | 
			
		||||
      await page.waitForNavigation({
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (this.page_context.url() == this.url) {
 | 
			
		||||
        // 📂 Save session context to avoid re-login
 | 
			
		||||
        await this.saveContext();
 | 
			
		||||
        console.log(`✅ [${this.id}] Login successful!`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`❌ [${this.id}] Login Failure!`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(
 | 
			
		||||
        `❌ [${this.id}] Error during login process:`,
 | 
			
		||||
        error.message
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      global.IS_CLEANING = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listen_events = async () => {
 | 
			
		||||
    if (this.page_context) return;
 | 
			
		||||
 | 
			
		||||
    await this.puppeteer_connect();
 | 
			
		||||
    await this.action();
 | 
			
		||||
 | 
			
		||||
    this.reloadInterval = setInterval(async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        if (this.page_context && !this.page_context.isClosed()) {
 | 
			
		||||
          console.log(`🔄 [${this.id}] Reloading page...`);
 | 
			
		||||
          await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
			
		||||
          console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(
 | 
			
		||||
            `❌ [${this.id}] Page context is closed. Stopping reload.`
 | 
			
		||||
          );
 | 
			
		||||
          clearInterval(this.reloadInterval);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
			
		||||
      }
 | 
			
		||||
    }, 60000); // 1p reload
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,479 @@
 | 
			
		|||
import _ from "lodash";
 | 
			
		||||
import { pushPrice, updateBid } from "../../system/apis/bid.js";
 | 
			
		||||
import configs from "../../system/config.js";
 | 
			
		||||
import { delay, isTimeReached, removeFalsyValues } from "../../system/utils.js";
 | 
			
		||||
import { ProductBid } from "../product-bid.js";
 | 
			
		||||
import { sendMessage } from "../../system/apis/notification.js";
 | 
			
		||||
 | 
			
		||||
export class PicklesProductBid extends ProductBid {
 | 
			
		||||
  constructor({ ...prev }) {
 | 
			
		||||
    super(prev);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleUpdateBid({
 | 
			
		||||
    lot_id,
 | 
			
		||||
    close_time,
 | 
			
		||||
    name,
 | 
			
		||||
    current_price,
 | 
			
		||||
    reserve_price,
 | 
			
		||||
  }) {
 | 
			
		||||
    const response = await updateBid(this.id, {
 | 
			
		||||
      lot_id,
 | 
			
		||||
      close_time,
 | 
			
		||||
      name,
 | 
			
		||||
      current_price,
 | 
			
		||||
      reserve_price: Number(reserve_price) || 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (response) {
 | 
			
		||||
      this.lot_id = response.lot_id;
 | 
			
		||||
      this.close_time = response.close_time;
 | 
			
		||||
      this.start_bid_time = response.start_bid_time;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetchFromPage = async (url) => {
 | 
			
		||||
    return await this.page_context.evaluate(async (url) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch(url, {
 | 
			
		||||
          method: "GET",
 | 
			
		||||
          headers: {
 | 
			
		||||
            "Content-Type": "application/json",
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        return await res.json();
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        return { error: err.message };
 | 
			
		||||
      }
 | 
			
		||||
    }, url);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  detailData = async () => {
 | 
			
		||||
    return await this.fetchFromPage(
 | 
			
		||||
      configs.WEB_CONFIGS.PICKLES.API_DETAIL_PRODUCT(this.model)
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  getName = async () => {
 | 
			
		||||
    if (!this.page_context) return null;
 | 
			
		||||
    try {
 | 
			
		||||
      return await this.page_context.$eval(
 | 
			
		||||
        "#pd-ph-header > div:first-child > div > div:nth-child(2) > div > h1",
 | 
			
		||||
        (el) => el.textContent
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(
 | 
			
		||||
        "%cmodels/pickles.com.au/pickles-product-bid.js:60 error.message",
 | 
			
		||||
        "color: #007acc;",
 | 
			
		||||
        error.message
 | 
			
		||||
      );
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  update = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!this.page_context) return;
 | 
			
		||||
 | 
			
		||||
      const result = await this.detailData();
 | 
			
		||||
 | 
			
		||||
      if (!result || !result[0]) return;
 | 
			
		||||
 | 
			
		||||
      const { item, bidding } = result[0];
 | 
			
		||||
 | 
			
		||||
      const name = await this.getName();
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        "%cmodels/pickles.com.au/pickles-product-bid.js:83 item",
 | 
			
		||||
        "color: #007acc;",
 | 
			
		||||
        bidding
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
 | 
			
		||||
      const data = removeFalsyValues(
 | 
			
		||||
        {
 | 
			
		||||
          lot_id: String(item?.id) || null,
 | 
			
		||||
          reserve_price: bidding?.minimumBidAmount || null,
 | 
			
		||||
          current_price: bidding?.currentActualBid || null,
 | 
			
		||||
          close_time: new Date(item?.itemBidEndTimestamp).toUTCString() || null,
 | 
			
		||||
          name: name || null,
 | 
			
		||||
        },
 | 
			
		||||
        ["close_time"]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(`🚀 [${this.id}] Processed data ready for update`);
 | 
			
		||||
 | 
			
		||||
      // 📌 Gửi dữ liệu cập nhật lên hệ thống
 | 
			
		||||
      await this.handleUpdateBid(data);
 | 
			
		||||
 | 
			
		||||
      await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
			
		||||
 | 
			
		||||
      await this.page_context.waitForNavigation({
 | 
			
		||||
        timeout: 8000,
 | 
			
		||||
        waitUntil: "domcontentloaded",
 | 
			
		||||
      });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log("Error Update", error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  submitBid() {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      if (!this.page_context || !this.lot_id) {
 | 
			
		||||
        console.log(`[${this.id}] Page context or model is missing.`);
 | 
			
		||||
        reject("Context is not define");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
 | 
			
		||||
 | 
			
		||||
        const result = await this.page_context.evaluate(
 | 
			
		||||
          async (bidAmount, lotRef, url) => {
 | 
			
		||||
            const response = await fetch(url, {
 | 
			
		||||
              method: "POST",
 | 
			
		||||
              headers: {
 | 
			
		||||
                "Content-Type": "application/json",
 | 
			
		||||
              },
 | 
			
		||||
              body: JSON.stringify({
 | 
			
		||||
                itemId: lotRef,
 | 
			
		||||
                bidValues: {
 | 
			
		||||
                  activity: "BID",
 | 
			
		||||
                  maxBid: bidAmount, // giá trị tối đa của sản phẩm
 | 
			
		||||
                  roundedMaxBid: bidAmount, // giá trị tối đa của sản phẩm
 | 
			
		||||
                  submittedBuyNowValue: null,
 | 
			
		||||
                },
 | 
			
		||||
                buyerFeeCalculated: false,
 | 
			
		||||
                buyerFees: null,
 | 
			
		||||
                dashboardRedirectUrl: null,
 | 
			
		||||
                itemTitle: null,
 | 
			
		||||
                productLine: null,
 | 
			
		||||
                registrationRequired: false,
 | 
			
		||||
                totalAmount: null,
 | 
			
		||||
                updateDetailsRequired: null,
 | 
			
		||||
              }),
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!response.ok) {
 | 
			
		||||
              throw new Error(`HTTP ${response.status}`);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return await response.json();
 | 
			
		||||
          },
 | 
			
		||||
          this.max_price + this.plus_price,
 | 
			
		||||
          this.lot_id,
 | 
			
		||||
          configs.WEB_CONFIGS.PICKLES.API_CHECKOUT
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        console.log("🧾 API Bid Result:", {
 | 
			
		||||
          bid_amount: this.max_price + this.plus_price,
 | 
			
		||||
          result,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!result?.confirmationRequest) reject("Api call failure");
 | 
			
		||||
 | 
			
		||||
        resolve(result);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
 | 
			
		||||
        reject(err);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handlePlaceBid() {
 | 
			
		||||
    // Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
 | 
			
		||||
    if (!this.page_context) {
 | 
			
		||||
      console.log(
 | 
			
		||||
        `⚠️ [${this.id}] No page context found, aborting bid process.`
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
    // Kiểm tra xem đấu giá đã đang diễn ra chưa. Nếu có thì không thực hiện nữa
 | 
			
		||||
    if (global[`IS_PLACE_BID-${this.id}`]) {
 | 
			
		||||
      console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log(`🔄 [${this.id}] Starting bid process...`);
 | 
			
		||||
      // Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại
 | 
			
		||||
      global[`IS_PLACE_BID-${this.id}`] = true;
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa
 | 
			
		||||
      if (this.current_price > this.max_price + this.plus_price) {
 | 
			
		||||
        console.log(`⚠️ [${this.id}] Outbid bid`);
 | 
			
		||||
        return; // Nếu giá hiện tại vượt quá mức giá tối đa thì dừng lại
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra thời gian đấu giá
 | 
			
		||||
      if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
 | 
			
		||||
            this.name || "None"
 | 
			
		||||
          }`
 | 
			
		||||
        );
 | 
			
		||||
        return; // Nếu chưa đến giờ đấu giá thì bỏ qua
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Đợi lấy thông tin API để kiểm tra tình trạng đấu giá hiện tại
 | 
			
		||||
      const response = await this.detailData();
 | 
			
		||||
 | 
			
		||||
      if (!response) {
 | 
			
		||||
        console.log(`[${this.id}] Can't get info data`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { bidding } = response[0];
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        "%cmodels/pickles.com.au/pickles-product-bid.js:157 response",
 | 
			
		||||
        "color: #007acc;",
 | 
			
		||||
        bidding
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // // Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
 | 
			
		||||
      const shouldStop =
 | 
			
		||||
        !response ||
 | 
			
		||||
        bidding?.currentActualBid > this.max_price + this.plus_price ||
 | 
			
		||||
        (bidding?.userItemBidStatus &&
 | 
			
		||||
          bidding?.userItemBidStatus?.type !== "OUTBID") ||
 | 
			
		||||
        !bidding?.minimumBidAmount ||
 | 
			
		||||
        bidding.minimumBidAmount > this.max_price + this.plus_price;
 | 
			
		||||
 | 
			
		||||
      if (shouldStop) {
 | 
			
		||||
        console.log(`⚠️ [${this.id}] Stop bidding:`, {
 | 
			
		||||
          reservePrice: bidding?.minimumBidAmount,
 | 
			
		||||
          currentBidAmount: response?.currentBidAmount,
 | 
			
		||||
          maxBidAmount: response?.maxBidAmount,
 | 
			
		||||
        });
 | 
			
		||||
        return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Tìm bid history lớn nhất từ các lịch sử đấu giá của item
 | 
			
		||||
      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
      console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
			
		||||
 | 
			
		||||
      // Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua
 | 
			
		||||
      if (
 | 
			
		||||
        bidHistoriesItem &&
 | 
			
		||||
        bidHistoriesItem?.price == this.current_price &&
 | 
			
		||||
        this.max_price + this.plus_price == response?.maxBidAmount
 | 
			
		||||
      ) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.reserve_price <= 0) {
 | 
			
		||||
        console.log(`[${this.reserve_price}]`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        `===============Start call to submit [${this.id}] ================`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // waiting 2s
 | 
			
		||||
      await delay(2000);
 | 
			
		||||
 | 
			
		||||
      // Nếu chưa bid, thực hiện đặt giá
 | 
			
		||||
      console.log(
 | 
			
		||||
        `💰 [${this.id}] Placing a bid with amount: ${this.max_price}`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Gửi bid qua API và nhận kết quả
 | 
			
		||||
      const result = await this.submitBid();
 | 
			
		||||
 | 
			
		||||
      // Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
 | 
			
		||||
      if (!result || !result?.confirmationRequest) {
 | 
			
		||||
        console.log(
 | 
			
		||||
          "%cmodels/pickles.com.au/pickles-product-bid.js:289 Error when call plance bid",
 | 
			
		||||
          "color: #007acc;",
 | 
			
		||||
          "Error when call plance bid"
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log({ result });
 | 
			
		||||
 | 
			
		||||
      // Gửi thông báo đã đấu giá thành công
 | 
			
		||||
      // sendMessage(this);
 | 
			
		||||
 | 
			
		||||
      pushPrice({
 | 
			
		||||
        bid_id: this.id,
 | 
			
		||||
        price: result?.yourBid || 0,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await this.page_context.evaluate(() => location.reload());
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Bid placed successfully!`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi
 | 
			
		||||
      console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành
 | 
			
		||||
      console.log(`🔚 [${this.id}] Resetting bid flag.`);
 | 
			
		||||
      global[`IS_PLACE_BID-${this.id}`] = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isOutBid = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      // Chờ tối đa 10s cho element xuất hiện
 | 
			
		||||
      const element = await page.waitForSelector(
 | 
			
		||||
        '[data-testid="pd-pbsb-bit-status-banner"]',
 | 
			
		||||
        {
 | 
			
		||||
          timeout: 10000, // 10s
 | 
			
		||||
          visible: true, // chỉ accept khi element hiện ra
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (!element) return false; // không có thì return false
 | 
			
		||||
 | 
			
		||||
      // Lấy innerHTML của element
 | 
			
		||||
      const innerHTML = await page.evaluate((el) => el.innerHTML, element);
 | 
			
		||||
 | 
			
		||||
      console.log(
 | 
			
		||||
        "%cmodels/pickles.com.au/pickles-product-bid.js:339 {in}",
 | 
			
		||||
        "color: #007acc;",
 | 
			
		||||
        { innerHTML }
 | 
			
		||||
      );
 | 
			
		||||
      // Kiểm tra có từ "outbid" không
 | 
			
		||||
      return innerHTML.includes("outbid");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Nếu lỗi (timeout hoặc gì đó) => return false
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  async trackingOutbid() {
 | 
			
		||||
    if (!this.page_context) return;
 | 
			
		||||
 | 
			
		||||
    if (global[`TRACKING_PROCRESS_${this.id}`]) {
 | 
			
		||||
      console.log(`🔄 [${this.id}] Removing previous response listeners...`);
 | 
			
		||||
      clearInterval(global[`TRACKING_PROCRESS_${this.id}`]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      global[`TRACKING_PROCRESS_${this.id}`] = setInterval(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
          const result = await this.detailData();
 | 
			
		||||
 | 
			
		||||
          if (!result) return;
 | 
			
		||||
 | 
			
		||||
          console.log(`📈 [${this.id}] Bid data: `, result);
 | 
			
		||||
 | 
			
		||||
          const { item, bidding } = result[0];
 | 
			
		||||
 | 
			
		||||
          console.log(
 | 
			
		||||
            `📊 [${this.id}] API Info - minimumBidAmount: ${bidding?.minimumBidAmount}, currentActualBid: ${bidding?.currentActualBid}`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
 | 
			
		||||
          const reversePrice = bidding?.currentActualBid;
 | 
			
		||||
          const currentBidAmount = bidding?.currentActualBid;
 | 
			
		||||
          const maxBidAmount = bidding?.buyerCurrentBid?.maximumBid;
 | 
			
		||||
          console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
 | 
			
		||||
 | 
			
		||||
          // Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
 | 
			
		||||
          const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
			
		||||
          console.log(
 | 
			
		||||
            `📈 [${this.id}] Highest local bid: ${
 | 
			
		||||
              bidHistoriesItem?.price ?? "N/A"
 | 
			
		||||
            }`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (!this.close_time || !this.lot_id || !this.current_price) return;
 | 
			
		||||
 | 
			
		||||
          // Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
 | 
			
		||||
          if (
 | 
			
		||||
            (!bidHistoriesItem && maxBidAmount) ||
 | 
			
		||||
            (bidHistoriesItem?.price != currentBidAmount &&
 | 
			
		||||
              currentBidAmount == maxBidAmount)
 | 
			
		||||
          ) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`
 | 
			
		||||
            );
 | 
			
		||||
            pushPrice({
 | 
			
		||||
              bid_id: this.id,
 | 
			
		||||
              price: Number(currentBidAmount),
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp
 | 
			
		||||
          if (
 | 
			
		||||
            reversePrice <= this.max_price + this.plus_price &&
 | 
			
		||||
            currentBidAmount <= this.max_price + this.plus_price &&
 | 
			
		||||
            this.max_price != maxBidAmount &&
 | 
			
		||||
            this.histories.length > 0 &&
 | 
			
		||||
            bidding?.currentMaximumBid !== this.max_price + this.plus_price
 | 
			
		||||
          ) {
 | 
			
		||||
            console.log(
 | 
			
		||||
              `⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`
 | 
			
		||||
            );
 | 
			
		||||
            await this.handlePlaceBid();
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error(`🚨 [${this.id}] Error parsing API response:`, error);
 | 
			
		||||
        }
 | 
			
		||||
      }, 5000);
 | 
			
		||||
 | 
			
		||||
      console.log(`✅ [${this.id}] Navigation setup complete.`);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`❌ [${this.id}] Error during navigationnnnn:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async gotoLink() {
 | 
			
		||||
    const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
    if (page.isClosed()) {
 | 
			
		||||
      console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log(`🔄 [${this.id}] Starting the bidding process...`);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
      console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
 | 
			
		||||
 | 
			
		||||
      console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
 | 
			
		||||
      console.log(`🛠️ [${this.id}] Setting custom user agent...`);
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      console.log(`🎯 [${this.id}] Listening for API responses...`);
 | 
			
		||||
 | 
			
		||||
      // tracking out bid
 | 
			
		||||
      this.trackingOutbid();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`❌ [${this.id}] Error during navigation:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  action = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
      // 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
 | 
			
		||||
      if (!page.url() || !page.url().includes(this.url)) {
 | 
			
		||||
        console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
 | 
			
		||||
        await this.gotoLink();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await this.handlePlaceBid();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,10 +1,8 @@
 | 
			
		|||
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 { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
 | 
			
		||||
import { Bid } from './bid.js';
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import BID_TYPE from "../system/bid-type.js";
 | 
			
		||||
import browser from "../system/browser.js";
 | 
			
		||||
import { getPathProfile } from "../system/utils.js";
 | 
			
		||||
import { Bid } from "./bid.js";
 | 
			
		||||
 | 
			
		||||
export class ProductBid extends Bid {
 | 
			
		||||
  id;
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +102,9 @@ export class ProductBid extends Bid {
 | 
			
		|||
 | 
			
		||||
  puppeteer_connect = async () => {
 | 
			
		||||
    if (!this.parent_browser_context) {
 | 
			
		||||
            console.log(`❌ Connect fail. parent_browser_context is null: ${this.id}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `❌ Connect fail. parent_browser_context is null: ${this.id}`
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ export class ProductBid extends Bid {
 | 
			
		|||
 | 
			
		||||
    if (!fs.existsSync(filePath)) return false;
 | 
			
		||||
 | 
			
		||||
        const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
 | 
			
		||||
    const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
 | 
			
		||||
 | 
			
		||||
    // Restore Cookies
 | 
			
		||||
    await context.setCookie(...contextData.cookies);
 | 
			
		||||
| 
						 | 
				
			
			@ -139,21 +139,23 @@ export class ProductBid extends Bid {
 | 
			
		|||
    const page = this.page_context;
 | 
			
		||||
 | 
			
		||||
    if (page.isClosed()) {
 | 
			
		||||
            console.error('❌ Page has been closed, cannot navigate.');
 | 
			
		||||
      console.error("❌ Page has been closed, cannot navigate.");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        console.log('🔄 Starting the bidding process...');
 | 
			
		||||
    console.log("🔄 Starting the bidding process...");
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
            await page.goto(this.url, { waitUntil: 'networkidle2' });
 | 
			
		||||
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
			
		||||
      console.log(`✅ Navigated to: ${this.url}`);
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
            await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
 | 
			
		||||
            console.log('👀 Brought the tab to the foreground.');
 | 
			
		||||
      await page.setUserAgent(
 | 
			
		||||
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
			
		||||
      );
 | 
			
		||||
      console.log("👀 Brought the tab to the foreground.");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
            console.error('❌ Error during navigation:', error);
 | 
			
		||||
      console.error("❌ Error during navigation:", error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,18 @@
 | 
			
		|||
import * as fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
 | 
			
		||||
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
 | 
			
		||||
import { LangtonsApiBid } from '../models/langtons.com.au/langtons-api-bid.js';
 | 
			
		||||
import { LangtonsProductBid } from '../models/langtons.com.au/langtons-product-bid.js';
 | 
			
		||||
import configs from '../system/config.js';
 | 
			
		||||
import CONSTANTS from '../system/constants.js';
 | 
			
		||||
import { sanitizeFileName } from '../system/utils.js';
 | 
			
		||||
import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js';
 | 
			
		||||
import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-product-bid.js';
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
 | 
			
		||||
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
 | 
			
		||||
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
 | 
			
		||||
import { LangtonsProductBid } from "../models/langtons.com.au/langtons-product-bid.js";
 | 
			
		||||
import { LawsonsApiBid } from "../models/lawsons.com.au/lawsons-api-bid.js";
 | 
			
		||||
import { LawsonsProductBid } from "../models/lawsons.com.au/lawsons-product-bid.js";
 | 
			
		||||
import { PicklesApiBid } from "../models/pickles.com.au/pickles-api-bid.js";
 | 
			
		||||
import { PicklesProductBid } from "../models/pickles.com.au/pickles-product-bid.js";
 | 
			
		||||
import configs from "../system/config.js";
 | 
			
		||||
import CONSTANTS from "../system/constants.js";
 | 
			
		||||
import { sanitizeFileName } from "../system/utils.js";
 | 
			
		||||
 | 
			
		||||
// Time to update
 | 
			
		||||
const TIME = 30 * 1000;
 | 
			
		||||
 | 
			
		||||
export const handleCloseRemoveProduct = (data) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +36,9 @@ export const createBidProduct = (web, data) => {
 | 
			
		|||
    case configs.WEB_URLS.LAWSONS: {
 | 
			
		||||
      return new LawsonsProductBid({ ...data });
 | 
			
		||||
    }
 | 
			
		||||
    case configs.WEB_URLS.PICKLES: {
 | 
			
		||||
      return new PicklesProductBid({ ...data });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,12 +53,18 @@ export const createApiBid = (web) => {
 | 
			
		|||
    case configs.WEB_URLS.LAWSONS: {
 | 
			
		||||
      return new LawsonsApiBid({ ...web });
 | 
			
		||||
    }
 | 
			
		||||
    case configs.WEB_URLS.PICKLES: {
 | 
			
		||||
      return new PicklesApiBid({ ...web });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deleteProfile = (data) => {
 | 
			
		||||
  if (!data?.origin_url) return false;
 | 
			
		||||
    const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(data?.origin_url) + '.json');
 | 
			
		||||
  const filePath = path.join(
 | 
			
		||||
    CONSTANTS.PROFILE_PATH,
 | 
			
		||||
    sanitizeFileName(data?.origin_url) + ".json"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (fs.existsSync(filePath)) {
 | 
			
		||||
    fs.unlinkSync(filePath);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,20 @@
 | 
			
		|||
import axios from '../axios.js';
 | 
			
		||||
import fs from 'fs';
 | 
			
		||||
import axios from "../axios.js";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
export const getBids = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: 'bids',
 | 
			
		||||
      method: "GET",
 | 
			
		||||
      url: "bids",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ DATA IS NOT FOUND ON SERVER');
 | 
			
		||||
      console.log("❌ DATA IS NOT FOUND ON SERVER");
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return data.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER (GET BIDS): ', error);
 | 
			
		||||
    console.log("❌ ERROR IN SERVER (GET BIDS): ", error);
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -22,19 +22,19 @@ export const getBids = async () => {
 | 
			
		|||
export const updateBid = async (id, values) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'PUT',
 | 
			
		||||
            url: 'bids/' + id,
 | 
			
		||||
      method: "PUT",
 | 
			
		||||
      url: "bids/" + id,
 | 
			
		||||
      data: values,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ UPDATE FAILURE (UPDATE BID)');
 | 
			
		||||
      console.log("❌ UPDATE FAILURE (UPDATE BID)");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return data.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
 | 
			
		||||
    console.log("❌ ERROR IN SERVER: (UPDATE BID) ", error.response);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -42,18 +42,18 @@ export const updateBid = async (id, values) => {
 | 
			
		|||
export const outBid = async (id) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: 'bids/out-bid/' + id,
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      url: "bids/out-bid/" + id,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ OUT BID UPDATE FAILURE');
 | 
			
		||||
      console.log("❌ OUT BID UPDATE FAILURE");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return data.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER (OUT BID UPDATE): ', error);
 | 
			
		||||
    console.log("❌ ERROR IN SERVER (OUT BID UPDATE): ", error);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -61,19 +61,19 @@ export const outBid = async (id) => {
 | 
			
		|||
export const pushPrice = async (values) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: 'bid-histories',
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      url: "bid-histories",
 | 
			
		||||
      data: values,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ PUSH PRICE FAILURE');
 | 
			
		||||
      console.log("❌ PUSH PRICE FAILURE");
 | 
			
		||||
      return { status: false, data: [] };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { status: true, data: data.data };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error?.response);
 | 
			
		||||
    console.log("❌ ERROR IN SERVER (PUSH PRICE): ", error?.response);
 | 
			
		||||
    return { status: false, data: [] };
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -81,21 +81,21 @@ export const pushPrice = async (values) => {
 | 
			
		|||
export const updateStatusByPrice = async (id, current_price) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: 'bids/update-status/' + id,
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      url: "bids/update-status/" + id,
 | 
			
		||||
      data: {
 | 
			
		||||
        current_price: Number(current_price) | 0,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ UPDATE STATUS BY PRICE FAILURE');
 | 
			
		||||
      console.log("❌ UPDATE STATUS BY PRICE FAILURE");
 | 
			
		||||
      return { status: false, data: [] };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { status: true, data: data.data };
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', {
 | 
			
		||||
    console.log("❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ", {
 | 
			
		||||
      // response: error.response,
 | 
			
		||||
      message: error.message,
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -106,9 +106,9 @@ export const updateStatusByPrice = async (id, current_price) => {
 | 
			
		|||
export const updateStatusWork = async (item, filePath) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      headers: {
 | 
			
		||||
                'Content-Type': 'multipart/form-data',
 | 
			
		||||
        "Content-Type": "multipart/form-data",
 | 
			
		||||
      },
 | 
			
		||||
      url: `bids/update-status-work/${item.type}/${item.id}`,
 | 
			
		||||
      data: {
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,25 @@ export const updateStatusWork = async (item, filePath) => {
 | 
			
		|||
 | 
			
		||||
    return response.data?.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.error('❌ Upload failed:', error.response?.data || error.message);
 | 
			
		||||
    console.error("❌ Upload failed:", error.response?.data || error.message);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const updateLoginStatus = async (data) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios({
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      url: `bids/update-login-status`,
 | 
			
		||||
      data,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return response.data?.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(
 | 
			
		||||
      "❌ UPDATE FAILURE (LOGIN STATUS):",
 | 
			
		||||
      error.response?.data || error.message
 | 
			
		||||
    );
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,21 +1,21 @@
 | 
			
		|||
import axios from '../axios.js';
 | 
			
		||||
import axios from "../axios.js";
 | 
			
		||||
 | 
			
		||||
export const sendMessage = async (values) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios({
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            url: 'notifications/send-messages',
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      url: "notifications/send-messages",
 | 
			
		||||
      data: values,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!data || !data?.data) {
 | 
			
		||||
            console.log('❌ UPDATE FAILURE (UPDATE Noti)');
 | 
			
		||||
      console.log("❌ UPDATE FAILURE (UPDATE Noti)");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return data.data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('❌ ERROR IN SERVER: (UPDATE Noti) ', error);
 | 
			
		||||
    console.log("❌ ERROR IN SERVER: (UPDATE Noti) ", error.response);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,27 +6,38 @@ const configs = {
 | 
			
		|||
    GRAYS: `https://www.grays.com`,
 | 
			
		||||
    LANGTONS: `https://www.langtons.com.au`,
 | 
			
		||||
    LAWSONS: `https://www.lawsons.com.au`,
 | 
			
		||||
    PICKLES: `https://www.pickles.com.au`,
 | 
			
		||||
  },
 | 
			
		||||
  WEB_CONFIGS: {
 | 
			
		||||
    GRAYS: {
 | 
			
		||||
      AUTO_CALL_API_TO_TRACKING: 3000,
 | 
			
		||||
            API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
 | 
			
		||||
      API_CALL_TO_TRACKING:
 | 
			
		||||
        "https://www.grays.com/api/Notifications/GetOutBidLots",
 | 
			
		||||
    },
 | 
			
		||||
    LANGTONS: {
 | 
			
		||||
      AUTO_CALL_API_TO_TRACKING: 5000,
 | 
			
		||||
            LOGIN_URL: 'https://www.langtons.com.au/account/login',
 | 
			
		||||
            API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
 | 
			
		||||
      LOGIN_URL: "https://www.langtons.com.au/account/login",
 | 
			
		||||
      API_CALL_TO_TRACKING:
 | 
			
		||||
        "https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData",
 | 
			
		||||
    },
 | 
			
		||||
    LAWSONS: {
 | 
			
		||||
            LOGIN_URL: 'https://www.lawsons.com.au/login?redirectUrl=/my-account/current-bids',
 | 
			
		||||
            // API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
 | 
			
		||||
      LOGIN_URL:
 | 
			
		||||
        "https://www.lawsons.com.au/login?redirectUrl=/my-account/current-bids",
 | 
			
		||||
      API_DETAIL_INFO: (model) => {
 | 
			
		||||
        return `https://www.lawsons.com.au/api/auctions/lot/v2/liveInfo/${model}`;
 | 
			
		||||
      },
 | 
			
		||||
      API_DETAIL_PRODUCT: (model) => {
 | 
			
		||||
        return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
 | 
			
		||||
      },
 | 
			
		||||
            API_CHECKOUT: 'https://www.lawsons.com.au/app/orderBid',
 | 
			
		||||
      API_CHECKOUT: "https://www.lawsons.com.au/app/orderBid",
 | 
			
		||||
    },
 | 
			
		||||
    PICKLES: {
 | 
			
		||||
      LOGIN_URL: "https://www.pickles.com.au/sign-in",
 | 
			
		||||
      API_DETAIL_PRODUCT: (model) => {
 | 
			
		||||
        return `https://www.pickles.com.au/api-website/buyer/ms-web-asset-aggregate/v2/api/assets/${model}/wap-item-details`;
 | 
			
		||||
      },
 | 
			
		||||
      API_CHECKOUT:
 | 
			
		||||
        "https://www.pickles.com.au/delegate/secured/bidding/confirm",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,31 @@
 | 
			
		|||
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) => {
 | 
			
		||||
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 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`;
 | 
			
		||||
    const fileName =
 | 
			
		||||
      type === CONSTANTS.TYPE_IMAGE.WORK
 | 
			
		||||
        ? `${imageName}.png`
 | 
			
		||||
        : `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
 | 
			
		||||
 | 
			
		||||
    const filePath = path.join(typeDir, fileName);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,15 +37,19 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
 | 
			
		|||
 | 
			
		||||
    // 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');
 | 
			
		||||
    const isPageResponsive = await page.evaluate(
 | 
			
		||||
      () => document.readyState === "complete"
 | 
			
		||||
    );
 | 
			
		||||
    if (!isPageResponsive) {
 | 
			
		||||
            console.log('🚫 Page is unresponsive, skipping snapshot.');
 | 
			
		||||
      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.');
 | 
			
		||||
    await page
 | 
			
		||||
      .waitForSelector("body", { visible: true, timeout: 15000 })
 | 
			
		||||
      .catch(() => {
 | 
			
		||||
        console.log("⚠️ Body selector not found, skipping snapshot.");
 | 
			
		||||
        return;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,18 +63,38 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
 | 
			
		|||
      await updateStatusWork(item, filePath);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
        console.log('Error when snapshot: ' + error.message);
 | 
			
		||||
    console.log("Error when snapshot: " + error.message);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 | 
			
		||||
 | 
			
		||||
export const safeClosePageReal = async (page) => {
 | 
			
		||||
  if (!page) return;
 | 
			
		||||
 | 
			
		||||
  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 page.close();
 | 
			
		||||
      await safeClosePageReal(page);
 | 
			
		||||
      // await page.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    item.page_context = undefined;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,11 +121,14 @@ export function extractNumber(str) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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 = []) {
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +168,9 @@ export function convertAETtoUTC(dateString) {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  // Tách chuỗi đầu vào
 | 
			
		||||
    const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
 | 
			
		||||
  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'");
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -138,11 +179,20 @@ export function convertAETtoUTC(dateString) {
 | 
			
		|||
 | 
			
		||||
  // 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;
 | 
			
		||||
  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));
 | 
			
		||||
  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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +225,36 @@ export function convertAETtoUTC(dateString) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function extractPriceNumber(priceString) {
 | 
			
		||||
    const cleaned = priceString.replace(/[^\d.]/g, '');
 | 
			
		||||
  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();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue