pickcels
This commit is contained in:
parent
173841c57c
commit
b13712a317
|
|
@ -38,6 +38,7 @@
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"uuid": "^11.0.5",
|
"uuid": "^11.0.5",
|
||||||
|
"yet-another-react-lightbox": "^3.22.0",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -1422,7 +1423,7 @@
|
||||||
"version": "19.0.3",
|
"version": "19.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
|
||||||
"integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
|
"integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
|
|
@ -7541,6 +7542,29 @@
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/zod": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"uuid": "^11.0.5",
|
"uuid": "^11.0.5",
|
||||||
|
"yet-another-react-lightbox": "^3.22.0",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { generateNestParams, handleError, handleSuccess } from '.';
|
import { generateNestParams, handleError, handleSuccess } from '.';
|
||||||
import axios from '../lib/axios';
|
import axios from '../lib/axios';
|
||||||
import { IAdmin } from '../system/type';
|
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'>) => {
|
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 {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,18 @@ export const shutdownTool = async () => {
|
||||||
handleError(error);
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const response = (error as AxiosError).response as Record<string, 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;
|
if (response.status === HttpStatusCode.Forbidden) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,14 +127,14 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||||
<TextInput readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
|
<TextInput withAsterisk readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
|
||||||
<TextInput size="sm" label="Email" {...form.getInputProps('email')} />
|
<TextInput withAsterisk size="sm" label="Email" {...form.getInputProps('email')} />
|
||||||
<TextInput className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
|
<TextInput withAsterisk className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
|
||||||
|
|
||||||
{!data && (
|
{!data && (
|
||||||
<>
|
<>
|
||||||
<PasswordInput size="sm" label="Password" {...form.getInputProps('password')} />
|
<PasswordInput withAsterisk size="sm" label="Password" {...form.getInputProps('password')} />
|
||||||
<PasswordInput size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
<PasswordInput withAsterisk size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
|
<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 withAsterisk 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="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||||
|
|
||||||
<Button className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
<Button className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||||
{'Grant'}
|
{'Grant'}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { default as ShowHistoriesModal } from './show-histories-modal';
|
export { default as ShowHistoriesModal } from './show-histories-modal';
|
||||||
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
|
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
|
||||||
|
export { default as ShowHistoriesBidPicklesApiModal } from './show-histories-bid-pickles-api-modal';
|
||||||
export { default as BidModal } from './bid-modal';
|
export { default as BidModal } from './bid-modal';
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
return histories.map((element) => (
|
return histories.map((element, index) => (
|
||||||
<Table.Tr key={element.LotId}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
|
<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>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||||
<Table.Td>{`AU $${element['Price']}`}</Table.Td>
|
<Table.Td>{`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())}</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Image, Modal, ModalProps, ScrollArea } from '@mantine/core';
|
import { ModalProps } from '@mantine/core';
|
||||||
|
import Lightbox from "yet-another-react-lightbox";
|
||||||
export default function ShowImageModal({ src, fallbackSrc, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
|
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 (
|
return (
|
||||||
<Modal
|
<Lightbox {...props} open={opened} close={onClose} slides={[{ src: src || fallbackSrc }]}
|
||||||
classNames={{
|
plugins={[Fullscreen, Zoom]}/>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,196 @@
|
||||||
import { Box, Button, Image, Text } from '@mantine/core';
|
import { Badge, Box, Button, Image, Text } from "@mantine/core";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from "socket.io-client";
|
||||||
import { getImagesWorking } from '../../apis/bid';
|
import { getImagesWorking } from "../../apis/bid";
|
||||||
import { IBid, IWebBid } from '../../system/type';
|
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
|
||||||
import ShowImageModal from './show-image-modal';
|
import { IBid, IWebBid } from "../../system/type";
|
||||||
|
import { cn, stringToColor } from "../../utils";
|
||||||
|
import ShowImageModal from "./show-image-modal";
|
||||||
export interface IWorkingPageProps {
|
export interface IWorkingPageProps {
|
||||||
data: (IBid | IWebBid) & { type: string };
|
data: (IBid | IWebBid) & { type: string };
|
||||||
socket: Socket;
|
socket: Socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
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);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||||
|
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
|
|
||||||
function isIBid(obj: IBid | IWebBid): obj is IBid {
|
const [payloadLoginStatus, setPayloadLoginStatus] = useState<{
|
||||||
return 'name' in obj;
|
data: IWebBid;
|
||||||
|
login_status: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const { statusTool } = useStatusToolStore();
|
||||||
|
|
||||||
|
function isIBid(obj: IBid | IWebBid): obj is IBid {
|
||||||
|
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 extractTime = (filename: string) => {
|
||||||
|
return Number(filename.split("-")[0]) || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = () => {
|
||||||
|
if (
|
||||||
|
statusTool &&
|
||||||
|
statusTool === "online" &&
|
||||||
|
payloadLoginStatus?.login_status
|
||||||
|
) {
|
||||||
|
return "logined";
|
||||||
}
|
}
|
||||||
|
return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
|
||||||
|
};
|
||||||
|
|
||||||
const renderUrl = ({ type, id }: (IBid | IWebBid) & { type: string }, name: string) => {
|
useEffect(() => {
|
||||||
return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`;
|
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));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractTime = (filename: string) => {
|
socket.on("working", updateImage);
|
||||||
return Number(filename.split('-')[0]) || 0;
|
|
||||||
|
return () => {
|
||||||
|
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
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const origin_url = isIBid(data) ? data.web_bid.origin_url : data.origin_url;
|
||||||
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(`login-status.${origin_url}`, onLoginStatus);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('working', updateImage);
|
socket.off(`login-status.${origin_url}`, onLoginStatus);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [data, socket]);
|
||||||
}, [socket, data.id, data.type]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const result = await getImagesWorking(data);
|
const result = await getImagesWorking(data);
|
||||||
|
|
||||||
if (!result || !result.data) return;
|
if (!result || !result.data) return;
|
||||||
|
|
||||||
const filename = result.data[0];
|
const filename = result.data[0];
|
||||||
|
|
||||||
setImageSrc(renderUrl(data, filename));
|
setImageSrc(renderUrl(data, filename));
|
||||||
setLastUpdate(new Date(extractTime(filename)));
|
setLastUpdate(new Date(extractTime(filename)));
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box className="rounded-md overflow-hidden relative shadow-lg">
|
<Box
|
||||||
<Image
|
className={cn("rounded-md overflow-hidden relative shadow-lg", {
|
||||||
radius="md"
|
["border border-green-800"]: payloadLoginStatus?.login_status,
|
||||||
h={300}
|
["border border-red-800"]: !payloadLoginStatus?.login_status,
|
||||||
style={{
|
})}
|
||||||
objectFit: 'cover',
|
>
|
||||||
}}
|
<Image
|
||||||
fallbackSrc={fallbackSrc}
|
radius="md"
|
||||||
src={imageSrc}
|
h={300}
|
||||||
/>
|
style={{
|
||||||
|
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">
|
<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>
|
<Text className="text-lg tracking-wide text-center font-bold">
|
||||||
{isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
|
{isIBid(data) ? data.name : "Tracking page"}
|
||||||
{isIBid(data) && <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>}
|
</Text>
|
||||||
<Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text>
|
{isIBid(data) && (
|
||||||
<Box className="flex items-center gap-4">
|
<Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>
|
||||||
<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
|
{isIBid(data) && (
|
||||||
</Button>
|
<Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
|
||||||
<Button
|
)}
|
||||||
target="_blank"
|
<Text className="text-sm italic opacity-80">
|
||||||
component="a"
|
{moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
|
||||||
size="xs"
|
</Text>
|
||||||
href={data.url || '/'}
|
<Box className="flex items-center gap-4">
|
||||||
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
|
<Button
|
||||||
>
|
size="xs"
|
||||||
Link
|
color="green"
|
||||||
</Button>
|
onClick={open}
|
||||||
</Box>
|
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
|
||||||
</Box>
|
>
|
||||||
</Box>
|
Show
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
target="_blank"
|
||||||
|
component="a"
|
||||||
|
size="xs"
|
||||||
|
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>
|
||||||
|
|
||||||
<ShowImageModal src={imageSrc || fallbackSrc} fallbackSrc={fallbackSrc} opened={opened} onClose={close} />
|
<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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
<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 withAsterisk 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')} />
|
<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">
|
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||||
{data ? 'Update' : 'Create'}
|
{data ? 'Update' : 'Create'}
|
||||||
|
|
|
||||||
|
|
@ -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,340 +1,410 @@
|
||||||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
|
import {
|
||||||
import _ from 'lodash';
|
IconAd,
|
||||||
import { useMemo, useRef, useState } from 'react';
|
IconAdOff,
|
||||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
IconEdit,
|
||||||
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
|
IconHammer,
|
||||||
import Table from '../lib/table/table';
|
IconHistory,
|
||||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
IconMenu,
|
||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
IconTrash,
|
||||||
import { mappingStatusColors } from '../system/constants';
|
} from "@tabler/icons-react";
|
||||||
import { IBid } from '../system/type';
|
import _ from "lodash";
|
||||||
import { formatTime } from '../utils';
|
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() {
|
export default function Bids() {
|
||||||
const refTableFn: TRefTableFn<IBid> = useRef({});
|
const refTableFn: TRefTableFn<IBid> = useRef({});
|
||||||
|
|
||||||
const [clickData, setClickData] = useState<IBid | null>(null);
|
const [clickData, setClickData] = useState<IBid | null>(null);
|
||||||
|
|
||||||
const { setConfirm } = useConfirmStore();
|
const { setConfirm } = useConfirmStore();
|
||||||
|
|
||||||
const [openedHistories, historiesModel] = useDisclosure(false);
|
const [openedHistories, historiesModel] = useDisclosure(false);
|
||||||
const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
|
const [openedHistoriesGraysApi, historiesGraysApiModel] =
|
||||||
const [openedBid, bidModal] = useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
|
||||||
const columns: IColumn<IBid>[] = [
|
const [openedHistoriesPicklesApi, historiesPicklesApiModel] =
|
||||||
{
|
useDisclosure(false);
|
||||||
key: 'id',
|
const [openedBid, bidModal] = useDisclosure(false);
|
||||||
title: 'ID',
|
|
||||||
typeFilter: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
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: 'model',
|
|
||||||
title: 'Model',
|
|
||||||
typeFilter: 'text',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
const columns: IColumn<IBid>[] = [
|
||||||
key: 'plus_price',
|
{
|
||||||
title: 'Plus price',
|
key: "id",
|
||||||
typeFilter: 'number',
|
title: "ID",
|
||||||
},
|
typeFilter: "number",
|
||||||
{
|
},
|
||||||
key: 'max_price',
|
{
|
||||||
title: 'Max price',
|
key: "name",
|
||||||
typeFilter: 'number',
|
title: "Name",
|
||||||
},
|
typeFilter: "text",
|
||||||
{
|
},
|
||||||
key: 'current_price',
|
{
|
||||||
title: 'Current price',
|
key: "web_bid",
|
||||||
typeFilter: 'number',
|
title: "Web",
|
||||||
},
|
typeFilter: "text",
|
||||||
{
|
renderRow(row) {
|
||||||
key: 'reserve_price',
|
return <span>{row.web_bid.origin_url}</span>;
|
||||||
title: 'Reserve price',
|
},
|
||||||
typeFilter: 'number',
|
},
|
||||||
},
|
{
|
||||||
{
|
key: "lot_id",
|
||||||
key: 'histories',
|
title: "Lot ID",
|
||||||
title: 'Current bid',
|
typeFilter: "text",
|
||||||
typeFilter: 'none',
|
},
|
||||||
renderRow(row) {
|
{
|
||||||
const bidPrice = _.maxBy(row.histories, 'price');
|
key: "model",
|
||||||
|
title: "Model",
|
||||||
|
typeFilter: "text",
|
||||||
|
},
|
||||||
|
|
||||||
return <Text>{bidPrice ? bidPrice.price : 'None'}</Text>;
|
{
|
||||||
},
|
key: "plus_price",
|
||||||
},
|
title: "Plus price",
|
||||||
{
|
typeFilter: "number",
|
||||||
key: 'start_bid_time',
|
},
|
||||||
title: 'Start bid',
|
{
|
||||||
typeFilter: 'text',
|
key: "max_price",
|
||||||
renderRow(row) {
|
title: "Max price",
|
||||||
return (
|
typeFilter: "number",
|
||||||
<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>
|
{
|
||||||
</Tooltip>
|
key: "current_price",
|
||||||
);
|
title: "Current price",
|
||||||
},
|
typeFilter: "number",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'close_time',
|
key: "reserve_price",
|
||||||
title: 'Close time',
|
title: "Reserve price",
|
||||||
typeFilter: 'text',
|
typeFilter: "number",
|
||||||
renderRow(row) {
|
},
|
||||||
return (
|
{
|
||||||
<Tooltip hidden={!row.close_time} label={row.close_time}>
|
key: "histories",
|
||||||
<Text size="sm">{row.close_time ? formatTime(row.close_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
|
title: "Current bid",
|
||||||
</Tooltip>
|
typeFilter: "none",
|
||||||
);
|
renderRow(row) {
|
||||||
},
|
const bidPrice = _.maxBy(row.histories, "price");
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
title: 'Status',
|
|
||||||
typeFilter: 'text',
|
|
||||||
renderRow(row) {
|
|
||||||
return (
|
|
||||||
<Box className="flex items-center justify-center">
|
|
||||||
<Badge color={mappingStatusColors[row.status]} size="sm">
|
|
||||||
{row.status}
|
|
||||||
</Badge>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleDelete = (bid: IBid) => {
|
return <Text>{bidPrice ? bidPrice.price : "None"}</Text>;
|
||||||
setConfirm({
|
},
|
||||||
title: 'Delete ?',
|
},
|
||||||
message: 'This bid will be delete',
|
{
|
||||||
handleOk: async () => {
|
key: "start_bid_time",
|
||||||
await deleteBid(bid);
|
title: "Start bid",
|
||||||
|
typeFilter: "text",
|
||||||
if (refTableFn.current?.fetchData) {
|
renderRow(row) {
|
||||||
refTableFn.current.fetchData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleBid = async (bid: IBid) => {
|
|
||||||
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 '),
|
|
||||||
handleOk: async () => {
|
|
||||||
await toggleBid(bid);
|
|
||||||
|
|
||||||
if (refTableFn.current?.fetchData) {
|
|
||||||
refTableFn.current.fetchData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
okButton: {
|
|
||||||
value: isEnable ? 'Disable ' : 'Enable ',
|
|
||||||
color: isEnable ? 'red' : 'blue',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const table = useMemo(() => {
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Tooltip hidden={!row.start_bid_time} label={row.start_bid_time}>
|
||||||
onClickRow={(row) => {
|
<Text size="sm">
|
||||||
window.open(row.url, '_blank');
|
{row.start_bid_time
|
||||||
}}
|
? formatTime(row.start_bid_time, "HH:mm:ss DD/MM/YYYY")
|
||||||
tableChildProps={{
|
: "None"}
|
||||||
trbody: {
|
</Text>
|
||||||
className: 'cursor-pointer',
|
</Tooltip>
|
||||||
},
|
|
||||||
}}
|
|
||||||
actionsOptions={{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
key: 'add',
|
|
||||||
title: 'Add',
|
|
||||||
callback: () => {
|
|
||||||
bidModal.open();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
title: 'Delete',
|
|
||||||
callback: (data) => {
|
|
||||||
if (!data.length) return;
|
|
||||||
setConfirm({
|
|
||||||
title: 'Delete',
|
|
||||||
message: `${data.length} will be delete`,
|
|
||||||
handleOk: async () => {
|
|
||||||
const result = await deletesBid(data);
|
|
||||||
|
|
||||||
if (!result) return;
|
|
||||||
if (refTableFn.current.fetchData) {
|
|
||||||
refTableFn.current.fetchData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
disabled: (data) => data.length <= 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
refTableFn={refTableFn}
|
|
||||||
striped
|
|
||||||
showLoading={true}
|
|
||||||
highlightOnHover
|
|
||||||
styleDefaultHead={{
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
width: 'fit-content',
|
|
||||||
}}
|
|
||||||
options={{
|
|
||||||
query: getBids,
|
|
||||||
pathToData: 'data.data',
|
|
||||||
keyOptions: {
|
|
||||||
last_page: 'lastPage',
|
|
||||||
per_page: 'perPage',
|
|
||||||
from: 'from',
|
|
||||||
to: 'to',
|
|
||||||
total: 'total',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
rows={[]}
|
|
||||||
withColumnBorders
|
|
||||||
showChooses={true}
|
|
||||||
withTableBorder
|
|
||||||
columns={columns}
|
|
||||||
actions={{
|
|
||||||
title: <Box className="w-full text-center">Action</Box>,
|
|
||||||
body: (row) => {
|
|
||||||
return (
|
|
||||||
<Menu shadow="md" width={200}>
|
|
||||||
<Menu.Target>
|
|
||||||
<Box onClick={(e) => e.stopPropagation()} className="flex w-full items-center justify-center">
|
|
||||||
<ActionIcon size="sm" variant="light">
|
|
||||||
<IconMenu size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Box>
|
|
||||||
</Menu.Target>
|
|
||||||
|
|
||||||
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
|
||||||
<Menu.Item
|
|
||||||
onClick={() => {
|
|
||||||
setClickData(row);
|
|
||||||
bidModal.open();
|
|
||||||
}}
|
|
||||||
leftSection={<IconEdit size={14} />}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Item
|
|
||||||
onClick={() => {
|
|
||||||
setClickData(row);
|
|
||||||
historiesModel.open();
|
|
||||||
}}
|
|
||||||
leftSection={<IconHistory size={14} />}
|
|
||||||
>
|
|
||||||
Histories
|
|
||||||
</Menu.Item>
|
|
||||||
{['https://www.grays.com'].includes(row?.web_bid.origin_url) && (
|
|
||||||
<Menu.Item
|
|
||||||
onClick={() => {
|
|
||||||
setClickData(row);
|
|
||||||
historiesGraysApiModel.open();
|
|
||||||
}}
|
|
||||||
leftSection={<IconHammer size={14} />}
|
|
||||||
>
|
|
||||||
Bids
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Menu.Item
|
|
||||||
disabled={row.status === 'win-bid'}
|
|
||||||
onClick={() => handleToggleBid(row)}
|
|
||||||
leftSection={row.status === 'biding' ? <IconAdOff size={14} /> : <IconAd size={14} />}
|
|
||||||
>
|
|
||||||
{row.status === 'biding' ? 'Disable' : 'Enable'}
|
|
||||||
</Menu.Item>
|
|
||||||
|
|
||||||
<Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
|
|
||||||
Delete
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
rowKey="id"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
},
|
||||||
}, []);
|
},
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
title: "Status",
|
||||||
|
typeFilter: "text",
|
||||||
|
renderRow(row) {
|
||||||
|
return (
|
||||||
|
<Box className="flex items-center justify-center">
|
||||||
|
<Badge color={mappingStatusColors[row.status]} size="sm">
|
||||||
|
{row.status}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleDelete = (bid: IBid) => {
|
||||||
|
setConfirm({
|
||||||
|
title: "Delete ?",
|
||||||
|
message: "This bid will be delete",
|
||||||
|
handleOk: async () => {
|
||||||
|
await deleteBid(bid);
|
||||||
|
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleBid = async (bid: IBid) => {
|
||||||
|
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 "),
|
||||||
|
handleOk: async () => {
|
||||||
|
await toggleBid(bid);
|
||||||
|
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
okButton: {
|
||||||
|
value: isEnable ? "Disable " : "Enable ",
|
||||||
|
color: isEnable ? "red" : "blue",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Table
|
||||||
{table}
|
onClickRow={(row) => {
|
||||||
|
window.open(row.url, "_blank");
|
||||||
|
}}
|
||||||
|
tableChildProps={{
|
||||||
|
trbody: {
|
||||||
|
className: "cursor-pointer",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
actionsOptions={{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
key: "add",
|
||||||
|
title: "Add",
|
||||||
|
callback: () => {
|
||||||
|
bidModal.open();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "delete",
|
||||||
|
title: "Delete",
|
||||||
|
callback: (data) => {
|
||||||
|
if (!data.length) return;
|
||||||
|
setConfirm({
|
||||||
|
title: "Delete",
|
||||||
|
message: `${data.length} will be delete`,
|
||||||
|
handleOk: async () => {
|
||||||
|
const result = await deletesBid(data);
|
||||||
|
|
||||||
<ShowHistoriesModal
|
if (!result) return;
|
||||||
opened={openedHistories}
|
if (refTableFn.current.fetchData) {
|
||||||
onClose={() => {
|
refTableFn.current.fetchData();
|
||||||
historiesModel.close();
|
|
||||||
setClickData(null);
|
|
||||||
}}
|
|
||||||
data={clickData}
|
|
||||||
/>
|
|
||||||
<BidModal
|
|
||||||
onUpdated={() => {
|
|
||||||
if (refTableFn.current?.fetchData) {
|
|
||||||
refTableFn.current.fetchData();
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disabled: (data) => data.length <= 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
refTableFn={refTableFn}
|
||||||
|
striped
|
||||||
|
showLoading={true}
|
||||||
|
highlightOnHover
|
||||||
|
styleDefaultHead={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "fit-content",
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
query: getBids,
|
||||||
|
pathToData: "data.data",
|
||||||
|
keyOptions: {
|
||||||
|
last_page: "lastPage",
|
||||||
|
per_page: "perPage",
|
||||||
|
from: "from",
|
||||||
|
to: "to",
|
||||||
|
total: "total",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
rows={[]}
|
||||||
|
withColumnBorders
|
||||||
|
showChooses={true}
|
||||||
|
withTableBorder
|
||||||
|
columns={columns}
|
||||||
|
actions={{
|
||||||
|
title: <Box className="w-full text-center">Action</Box>,
|
||||||
|
body: (row) => {
|
||||||
|
return (
|
||||||
|
<Menu shadow="md" width={200}>
|
||||||
|
<Menu.Target>
|
||||||
|
<Box
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex w-full items-center justify-center"
|
||||||
|
>
|
||||||
|
<ActionIcon size="sm" variant="light">
|
||||||
|
<IconMenu size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
setClickData(null);
|
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||||
}}
|
<Menu.Item
|
||||||
opened={openedBid}
|
onClick={() => {
|
||||||
onClose={() => {
|
setClickData(row);
|
||||||
bidModal.close();
|
bidModal.open();
|
||||||
|
}}
|
||||||
|
leftSection={<IconEdit size={14} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
setClickData(null);
|
<Menu.Item
|
||||||
}}
|
onClick={() => {
|
||||||
data={clickData}
|
setClickData(row);
|
||||||
/>
|
historiesModel.open();
|
||||||
|
}}
|
||||||
|
leftSection={<IconHistory size={14} />}
|
||||||
|
>
|
||||||
|
Histories
|
||||||
|
</Menu.Item>
|
||||||
|
{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} />}
|
||||||
|
>
|
||||||
|
Bids
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<ShowHistoriesBidGraysApiModal
|
<Menu.Item
|
||||||
onUpdated={() => {
|
disabled={row.status === "win-bid"}
|
||||||
if (refTableFn.current?.fetchData) {
|
onClick={() => handleToggleBid(row)}
|
||||||
refTableFn.current.fetchData();
|
leftSection={
|
||||||
|
row.status === "biding" ? (
|
||||||
|
<IconAdOff size={14} />
|
||||||
|
) : (
|
||||||
|
<IconAd size={14} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
{row.status === "biding" ? "Disable" : "Enable"}
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
setClickData(null);
|
<Menu.Item
|
||||||
}}
|
onClick={() => handleDelete(row)}
|
||||||
opened={openedHistoriesGraysApi}
|
leftSection={<IconTrash color="red" size={14} />}
|
||||||
onClose={() => {
|
>
|
||||||
historiesGraysApiModel.close();
|
Delete
|
||||||
|
</Menu.Item>
|
||||||
setClickData(null);
|
</Menu.Dropdown>
|
||||||
}}
|
</Menu>
|
||||||
data={clickData}
|
);
|
||||||
/>
|
},
|
||||||
</Box>
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{table}
|
||||||
|
<ShowHistoriesModal
|
||||||
|
opened={openedHistories}
|
||||||
|
onClose={() => {
|
||||||
|
historiesModel.close();
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
data={clickData}
|
||||||
|
/>
|
||||||
|
<BidModal
|
||||||
|
onUpdated={() => {
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
opened={openedBid}
|
||||||
|
onClose={() => {
|
||||||
|
bidModal.close();
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
data={clickData}
|
||||||
|
/>
|
||||||
|
{/* Grays */}
|
||||||
|
{openedHistoriesGraysApi && (
|
||||||
|
<ShowHistoriesBidGraysApiModal
|
||||||
|
onUpdated={() => {
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
opened={openedHistoriesGraysApi}
|
||||||
|
onClose={() => {
|
||||||
|
historiesGraysApiModel.close();
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
data={clickData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openedHistoriesPicklesApi && (
|
||||||
|
<ShowHistoriesBidPicklesApiModal
|
||||||
|
onUpdated={() => {
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
opened={true}
|
||||||
|
onClose={() => {
|
||||||
|
historiesPicklesApiModel.close();
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
data={clickData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,186 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Box, Button, LoadingOverlay, Text, Title } from '@mantine/core';
|
import {
|
||||||
import { useEffect, useRef, useState } from 'react';
|
Box,
|
||||||
import io from 'socket.io-client';
|
Button,
|
||||||
import { WorkingPage } from '../components/dashboard';
|
LoadingOverlay,
|
||||||
import { IBid, IWebBid } from '../system/type';
|
Text,
|
||||||
import { checkStatus } from '../apis/auth';
|
Title,
|
||||||
import { IconPower, IconRestore } from '@tabler/icons-react';
|
Tooltip,
|
||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
} from "@mantine/core";
|
||||||
import { resetTool, shutdownTool } from '../apis/dashboard';
|
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`, {
|
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
transports: ['websocket'],
|
transports: ["websocket"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function DashBoard() {
|
export default function DashBoard() {
|
||||||
const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
|
const [workingData, setWorkingData] = useState<
|
||||||
const { setConfirm } = useConfirmStore();
|
(IWebBid & { type: string })[] | (IBid & { type: string })[]
|
||||||
|
>([]);
|
||||||
|
const { setConfirm } = useConfirmStore();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const RETRY_CONNECT = useRef(2);
|
const { setStatusTool, statusTool } = useStatusToolStore();
|
||||||
|
|
||||||
|
const RETRY_CONNECT = useRef(2);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.connect();
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
socket.emit("getBidsData");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", async () => {
|
||||||
|
if (RETRY_CONNECT.current > 0) {
|
||||||
|
await checkStatus();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
socket.connect();
|
socket.connect();
|
||||||
|
|
||||||
socket.on('connect', () => {
|
RETRY_CONNECT.current--;
|
||||||
socket.emit('getBidsData');
|
return;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('disconnect', async () => {
|
socket.on("adminBidsUpdated", (data: IWebBid[]) => {
|
||||||
if (RETRY_CONNECT.current > 0) {
|
const array = data.reduce((prev, cur) => {
|
||||||
await checkStatus();
|
if (cur.children?.length > 0) {
|
||||||
|
prev = [...prev, ...cur.children];
|
||||||
|
}
|
||||||
|
prev.push(cur);
|
||||||
|
return prev;
|
||||||
|
}, [] as any[]);
|
||||||
|
|
||||||
socket.connect();
|
const newData = array.map((item) => {
|
||||||
|
if (item.children) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
type: "API_BID",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
RETRY_CONNECT.current--;
|
return {
|
||||||
return;
|
...item,
|
||||||
}
|
type: "PRODUCT_TAB",
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('adminBidsUpdated', (data: IWebBid[]) => {
|
|
||||||
const array = data.reduce((prev, cur) => {
|
|
||||||
if (cur.children?.length > 0) {
|
|
||||||
prev = [...prev, ...cur.children];
|
|
||||||
}
|
|
||||||
prev.push(cur);
|
|
||||||
return prev;
|
|
||||||
}, [] as any[]);
|
|
||||||
|
|
||||||
const newData = array.map((item) => {
|
|
||||||
if (item.children) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
type: 'API_BID',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
type: 'PRODUCT_TAB',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setWorkingData(newData);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log('🔌 Cleanup WebSocket listeners...');
|
|
||||||
socket.off('adminBidsUpdated');
|
|
||||||
socket.off('working');
|
|
||||||
socket.off('connect');
|
|
||||||
socket.off('disconnect');
|
|
||||||
socket.disconnect();
|
|
||||||
};
|
};
|
||||||
}, []);
|
});
|
||||||
|
setWorkingData(newData);
|
||||||
|
});
|
||||||
|
|
||||||
const handleResetTool = () => {
|
return () => {
|
||||||
setConfirm({
|
console.log("🔌 Cleanup WebSocket listeners...");
|
||||||
handleOk: async () => {
|
socket.off("adminBidsUpdated");
|
||||||
setLoading(true);
|
socket.off("working");
|
||||||
await resetTool();
|
socket.off("connect");
|
||||||
setLoading(false);
|
socket.off("disconnect");
|
||||||
},
|
socket.disconnect();
|
||||||
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' },
|
|
||||||
});
|
useEffect(() => {
|
||||||
|
const statusTool = async () => {
|
||||||
|
const result = await getStatusTool();
|
||||||
|
|
||||||
|
if (result?.data) {
|
||||||
|
setStatusTool(result?.data);
|
||||||
|
} else {
|
||||||
|
setStatusTool(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShutdownTool = () => {
|
const intervalId = setInterval(statusTool, 5000);
|
||||||
setConfirm({
|
|
||||||
handleOk: async () => {
|
return () => {
|
||||||
setLoading(true);
|
clearInterval(intervalId);
|
||||||
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' },
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
const handleResetTool = () => {
|
||||||
<Box>
|
setConfirm({
|
||||||
<Box className="flex items-center justify-between">
|
handleOk: async () => {
|
||||||
<Title order={2} mb="md">
|
setLoading(true);
|
||||||
Admin Dashboard
|
await resetTool();
|
||||||
</Title>
|
setLoading(false);
|
||||||
<Box className="flex gap-2">
|
},
|
||||||
<Button onClick={handleResetTool} leftSection={<IconRestore size={16} />} size="xs">
|
title: "Confirm tool reset",
|
||||||
Reset tool
|
message:
|
||||||
</Button>
|
"Are you sure you want to reset this tool? All current processes will be stopped and restarted.",
|
||||||
<Button onClick={handleShutdownTool} leftSection={<IconPower size={16} />} color="red" size="xs">
|
okButton: { value: "Ok", color: "blue" },
|
||||||
Shutdown tool
|
});
|
||||||
</Button>
|
};
|
||||||
</Box>
|
|
||||||
</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} />)}
|
|
||||||
|
|
||||||
{workingData.length <= 0 && (
|
const handleShutdownTool = () => {
|
||||||
<Box className="flex items-center justify-center col-span-4">
|
setConfirm({
|
||||||
<Text>No Pages</Text>
|
handleOk: async () => {
|
||||||
</Box>
|
setLoading(true);
|
||||||
)}
|
await shutdownTool();
|
||||||
</Box>
|
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" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
<LoadingOverlay visible={loading} />
|
return (
|
||||||
</Box>
|
<Box>
|
||||||
);
|
<Box className="flex items-center justify-between">
|
||||||
|
<Title order={2} mb="md">
|
||||||
|
Admin Dashboard
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Shutdown tool
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<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">
|
||||||
|
<Text>No Pages</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LoadingOverlay visible={loading} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,153 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from "tailwind-merge";
|
||||||
import moment from 'moment';
|
import moment from "moment";
|
||||||
export function cn(...args: ClassValue[]) {
|
export function cn(...args: ClassValue[]) {
|
||||||
return twMerge(clsx(args));
|
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);
|
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>>(
|
||||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
obj: T,
|
||||||
if (value || excludeKeys.includes(key as keyof T)) {
|
excludeKeys: (keyof T)[] = []
|
||||||
acc[key as keyof T] = value;
|
): Partial<T> {
|
||||||
}
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||||
return acc;
|
if (value || excludeKeys.includes(key as keyof T)) {
|
||||||
}, {} as Partial<T>);
|
acc[key as keyof T] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Partial<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidJSON(str: string): boolean {
|
export function isValidJSON(str: string): boolean {
|
||||||
if (!str || str.length <= 0) return false;
|
if (!str || str.length <= 0) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSON.parse(str);
|
JSON.parse(str);
|
||||||
return true;
|
return true;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyToClipboard(text: string, onSuccess?: () => void): void {
|
export function copyToClipboard(text: string, onSuccess?: () => void): void {
|
||||||
if (!navigator.clipboard) {
|
if (!navigator.clipboard) {
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement("textarea");
|
||||||
textarea.value = text;
|
textarea.value = text;
|
||||||
textarea.style.position = 'fixed';
|
textarea.style.position = "fixed";
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
textarea.select();
|
textarea.select();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
document.execCommand("copy");
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
} catch (err) {
|
} 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);
|
|
||||||
} else {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(text)
|
|
||||||
.then(() => {
|
|
||||||
if (onSuccess) onSuccess();
|
|
||||||
})
|
|
||||||
.catch((err) => console.error('Lỗi khi copy nội dung: ', err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
} else {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Lỗi khi copy nội dung: ", err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToFile(base64String: string, fileName: string): File {
|
export function base64ToFile(base64String: string, fileName: string): File {
|
||||||
const [header, base64Content] = base64String.split(',');
|
const [header, base64Content] = base64String.split(",");
|
||||||
|
|
||||||
const mimeTypeMatch = header.match(/:(.*?);/);
|
const mimeTypeMatch = header.match(/:(.*?);/);
|
||||||
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
|
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
|
||||||
throw new Error('Invalid base64 string');
|
throw new Error("Invalid base64 string");
|
||||||
}
|
}
|
||||||
const mimeType = mimeTypeMatch[1];
|
const mimeType = mimeTypeMatch[1];
|
||||||
|
|
||||||
const binaryString = atob(base64Content);
|
const binaryString = atob(base64Content);
|
||||||
|
|
||||||
const byteArray = new Uint8Array(binaryString.length);
|
const byteArray = new Uint8Array(binaryString.length);
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
byteArray[i] = binaryString.charCodeAt(i);
|
byteArray[i] = binaryString.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new File([byteArray], fileName, { type: mimeType });
|
return new File([byteArray], fileName, { type: mimeType });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toSlug(str: string, maxLength = 60): string {
|
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`
|
// 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
|
return normalizedStr
|
||||||
.replace(/[\u0300-\u036f]/g, '') // Xóa 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 "-"
|
.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
|
.trim() // Xóa khoảng trắng đầu/cuối
|
||||||
.replace(/\s+/g, '-') // Thay khoảng trắng bằng "-"
|
.replace(/\s+/g, "-") // Thay khoảng trắng bằng "-"
|
||||||
.replace(/-+/g, '-') // Gộp nhiều dấu "-" thành 1
|
.replace(/-+/g, "-") // Gộp nhiều dấu "-" thành 1
|
||||||
.toLowerCase() // Chuyển về chữ thường
|
.toLowerCase() // Chuyển về chữ thường
|
||||||
.slice(0, maxLength) // Giới hạn độ dài
|
.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 {
|
export function estimateReadingTimeInSeconds(
|
||||||
if (!content || typeof content !== 'string') return 0;
|
content: string,
|
||||||
|
wordsPerMinute = 200
|
||||||
|
): number {
|
||||||
|
if (!content || typeof content !== "string") return 0;
|
||||||
|
|
||||||
const wordCount = content.trim().split(/\s+/).length;
|
const wordCount = content.trim().split(/\s+/).length;
|
||||||
return Math.ceil((wordCount / wordsPerMinute) * 60);
|
return Math.ceil((wordCount / wordsPerMinute) * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractDomain(url: string): string | null {
|
export function extractDomain(url: string): string | null {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
return parsedUrl.origin;
|
return parsedUrl.origin;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"createdAt":1744861741554}
|
{"createdAt":1745827424853}
|
||||||
|
|
@ -2,18 +2,53 @@ import { Injectable } from '@nestjs/common';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AppResponse from 'src/response/app-response';
|
import AppResponse from 'src/response/app-response';
|
||||||
import { Bid } from '../entities/bid.entity';
|
import { Bid } from '../entities/bid.entity';
|
||||||
|
import { BidsService } from '../services/bids.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GraysApi {
|
export class GraysApi {
|
||||||
async getHistoriesBid(lot_id: Bid['lot_id']) {
|
|
||||||
try {
|
|
||||||
const response = await axios({
|
|
||||||
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data && response.data?.Bids) {
|
|
||||||
return AppResponse.toResponse(response.data.Bids);
|
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){
|
||||||
|
|
||||||
|
case 'https://www.grays.com': {
|
||||||
|
const response = await axios({
|
||||||
|
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data?.Bids) {
|
||||||
|
return AppResponse.toResponse(response.data.Bids);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResponse.toResponse([])
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResponse.toResponse([])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return AppResponse.toResponse([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return AppResponse.toResponse([]);
|
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';
|
import { DashboardService } from '../../services/dashboard.service';
|
||||||
|
|
||||||
@Controller('admin/dashboards')
|
@Controller('admin/dashboards')
|
||||||
|
|
@ -14,4 +14,9 @@ export class AdminDashboardController {
|
||||||
async shutdownTool() {
|
async shutdownTool() {
|
||||||
return await this.dashboardService.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 { WebBidsService } from '../../services/web-bids.service';
|
||||||
import { Event } from '../../utils/events';
|
import { Event } from '../../utils/events';
|
||||||
import AppResponse from '@/response/app-response';
|
import AppResponse from '@/response/app-response';
|
||||||
|
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
|
||||||
|
|
||||||
@Controller('bids')
|
@Controller('bids')
|
||||||
export class BidsController {
|
export class BidsController {
|
||||||
|
|
@ -68,17 +69,24 @@ export class BidsController {
|
||||||
return this.bidsService.updateStatusWork(id, type, image);
|
return this.bidsService.updateStatusWork(id, type, image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('update-login-status')
|
||||||
|
async updateLoginStatus(
|
||||||
|
@Body() data: ClientUpdateLoginStatusDto
|
||||||
|
) {
|
||||||
|
return await this.bidsService.emitLoginStatus(data)
|
||||||
|
}
|
||||||
|
|
||||||
@Post('test')
|
@Post('test')
|
||||||
async test(@Body('code') code: string) {
|
async test(@Body('code') code: string) {
|
||||||
const webBid = await this.webBidService.webBidRepo.findOne({
|
const webBid = await this.webBidService.webBidRepo.findOne({
|
||||||
// where: { id: 9 },
|
where: { id: 4 },
|
||||||
where: { id: 8 },
|
// where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.emit(Event.verifyCode(webBid), {
|
this.eventEmitter.emit(Event.verifyCode(webBid), {
|
||||||
code,
|
code,
|
||||||
// name: 'LAWSONS',
|
name: 'LAWSONS',
|
||||||
name: 'LANGTONS',
|
// name: 'LANGTONS',
|
||||||
web_bid: plainToClass(WebBid, webBid),
|
web_bid: plainToClass(WebBid, 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
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,17 @@ export class AdminBidGateway implements OnGatewayConnection {
|
||||||
this.server.emit(Event.WORKING, data);
|
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
|
// IMAP
|
||||||
this.imapService.connectIMAP();
|
this.imapService.connectIMAP();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import { WebBidsService } from './web-bids.service';
|
||||||
import { NotificationService } from '@/modules/notification/notification.service';
|
import { NotificationService } from '@/modules/notification/notification.service';
|
||||||
import { Event } from '../utils/events';
|
import { Event } from '../utils/events';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BidsService {
|
export class BidsService {
|
||||||
|
|
@ -273,7 +274,7 @@ export class BidsService {
|
||||||
const result = await this.bidsRepo.save({
|
const result = await this.bidsRepo.save({
|
||||||
...bid,
|
...bid,
|
||||||
...data,
|
...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
|
updated_at: new Date(), // Cập nhật timestamp
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -508,4 +509,12 @@ export class BidsService {
|
||||||
|
|
||||||
return AppResponse.toResponse(files);
|
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';
|
private readonly tool_name = 'auto-bid-tool';
|
||||||
|
|
||||||
async resetToolByName(toolName: string): Promise<string> {
|
async resetProcessByName(toolName: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Lấy danh sách process đang chạy
|
// Lấy danh sách process đang chạy
|
||||||
exec('pm2 jlist', (error, stdout, stderr) => {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Lấy danh sách process đang chạy
|
// Lấy danh sách process đang chạy
|
||||||
exec('pm2 jlist', (error, stdout, stderr) => {
|
exec('pm2 jlist', (error, stdout, stderr) => {
|
||||||
|
|
@ -77,7 +105,7 @@ export class DashboardService {
|
||||||
|
|
||||||
async resetTool() {
|
async resetTool() {
|
||||||
try {
|
try {
|
||||||
await this.resetToolByName(this.tool_name);
|
await this.resetProcessByName(this.tool_name);
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -87,11 +115,22 @@ export class DashboardService {
|
||||||
|
|
||||||
async shutdownTool() {
|
async shutdownTool() {
|
||||||
try {
|
try {
|
||||||
await this.shutdownToolByName(this.tool_name);
|
await this.shutdownProcessByName(this.tool_name);
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return AppResponse.toResponse(false);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,13 @@ export class Event {
|
||||||
public static BIDS_UPDATED = 'bidsUpdated';
|
public static BIDS_UPDATED = 'bidsUpdated';
|
||||||
public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
|
public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
|
||||||
public static WEB_UPDATED = 'webUpdated';
|
public static WEB_UPDATED = 'webUpdated';
|
||||||
|
public static LOGIN_STATUS = 'login-status';
|
||||||
|
|
||||||
public static verifyCode(data: WebBid) {
|
public static verifyCode(data: WebBid) {
|
||||||
return `${this.VERIFY_CODE}.${data.origin_url}`;
|
return `${this.VERIFY_CODE}.${data.origin_url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static statusLogin(data: WebBid) {
|
||||||
|
return `${this.LOGIN_STATUS}.${data.origin_url}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,21 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => {
|
||||||
type: 'mysql',
|
|
||||||
host: configService.get<string>('DB_HOST'),
|
return {
|
||||||
port: configService.get<number>('DB_PORT'),
|
type: 'mysql',
|
||||||
username: configService.get<string>('DB_USERNAME'),
|
host: configService.get<string>('DB_HOST'),
|
||||||
password: configService.get<string>('DB_PASSWORD'),
|
port: configService.get<number>('DB_PORT'),
|
||||||
database: configService.get<string>('DB_NAME'),
|
username: configService.get<string>('DB_USERNAME'),
|
||||||
charset: 'utf8mb4_unicode_ci',
|
password: configService.get<string>('DB_PASSWORD'),
|
||||||
entities: ['dist/**/*.entity{.ts,.js}'],
|
database: configService.get<string>('DB_NAME'),
|
||||||
synchronize:
|
charset: 'utf8mb4_unicode_ci',
|
||||||
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
|
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('_');
|
const match = url.split('_');
|
||||||
return match ? match[1] : null;
|
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,17 @@
|
||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
import pLimit from 'p-limit';
|
import pLimit from "p-limit";
|
||||||
import { io } from 'socket.io-client';
|
import { io } from "socket.io-client";
|
||||||
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
|
import {
|
||||||
import browser from './system/browser.js';
|
createApiBid,
|
||||||
import configs from './system/config.js';
|
createBidProduct,
|
||||||
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
|
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 { updateLoginStatus } from "./system/apis/bid.js";
|
||||||
|
|
||||||
global.IS_CLEANING = true;
|
global.IS_CLEANING = true;
|
||||||
|
|
||||||
|
|
@ -14,260 +20,335 @@ let MANAGER_BIDS = [];
|
||||||
const activeTasks = new Set();
|
const activeTasks = new Set();
|
||||||
|
|
||||||
const handleUpdateProductTabs = (data) => {
|
const handleUpdateProductTabs = (data) => {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
console.log('Data must be array');
|
console.log("Data must be array");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
|
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
|
||||||
|
|
||||||
const newDataManager = data.map(({ children, ...web }) => {
|
const newDataManager = data.map(({ children, ...web }) => {
|
||||||
const prevApiBid = managerBidMap.get(web.id);
|
const prevApiBid = managerBidMap.get(web.id);
|
||||||
|
|
||||||
const newChildren = children.map((item) => {
|
const newChildren = children.map((item) => {
|
||||||
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
|
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
|
||||||
|
|
||||||
if (prevProductTab) {
|
if (prevProductTab) {
|
||||||
prevProductTab.setNewData(item);
|
prevProductTab.setNewData(item);
|
||||||
|
|
||||||
return prevProductTab;
|
return prevProductTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBidProduct(web, item);
|
return createBidProduct(web, item);
|
||||||
});
|
|
||||||
|
|
||||||
if (prevApiBid) {
|
|
||||||
prevApiBid.setNewData({ children: newChildren, ...web });
|
|
||||||
return prevApiBid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createApiBid({ ...web, children: newChildren });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MANAGER_BIDS = newDataManager;
|
if (prevApiBid) {
|
||||||
|
prevApiBid.setNewData({ children: newChildren, ...web });
|
||||||
|
return prevApiBid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createApiBid({ ...web, children: newChildren });
|
||||||
|
});
|
||||||
|
|
||||||
|
MANAGER_BIDS = newDataManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracking = async () => {
|
const tracking = async () => {
|
||||||
console.log('🚀 Tracking process started...');
|
console.log("🚀 Tracking process started...");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Scanning active bids...');
|
console.log("🔍 Scanning active bids...");
|
||||||
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
||||||
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
||||||
return apiBid.listen_events();
|
return apiBid.listen_events();
|
||||||
}),
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Promise.allSettled(
|
||||||
|
productTabs.map(async (productTab) => {
|
||||||
|
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
||||||
|
|
||||||
|
// Xác định parent context
|
||||||
|
if (!productTab.parent_browser_context) {
|
||||||
|
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})`
|
||||||
|
);
|
||||||
|
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}`
|
||||||
);
|
);
|
||||||
|
await productTab.puppeteer_connect();
|
||||||
|
}
|
||||||
|
|
||||||
Promise.allSettled(
|
// Kiểm tra URL và điều hướng nếu cần
|
||||||
productTabs.map(async (productTab) => {
|
if ((await productTab.page_context.url()) !== productTab.url) {
|
||||||
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
console.log(
|
||||||
|
`🔄 Redirecting to new URL for Product ID: ${productTab.id}`
|
||||||
// Xác định parent context
|
|
||||||
if (!productTab.parent_browser_context) {
|
|
||||||
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})`);
|
|
||||||
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}`);
|
|
||||||
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}`);
|
|
||||||
await productTab.gotoLink();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cập nhật nếu cần thiết
|
|
||||||
if (shouldUpdateProductTab(productTab)) {
|
|
||||||
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
|
|
||||||
await productTab.update();
|
|
||||||
} else {
|
|
||||||
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}`);
|
|
||||||
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}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thực thi hành động
|
|
||||||
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
|
||||||
await productTab.action();
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
await productTab.gotoLink();
|
||||||
|
}
|
||||||
|
|
||||||
// Dọn dẹp tab không dùng
|
// Cập nhật nếu cần thiết
|
||||||
console.log('🧹 Cleaning up unused tabs...');
|
if (shouldUpdateProductTab(productTab)) {
|
||||||
clearLazyTab();
|
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
|
||||||
|
await productTab.update();
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Cập nhật trạng thái tracking
|
// Chờ first bid
|
||||||
console.log('📊 Tracking work status...');
|
if (!productTab.first_bid) {
|
||||||
workTracking();
|
console.log(
|
||||||
} catch (error) {
|
`🎯 Waiting for first bid for Product ID: ${productTab.id}`
|
||||||
console.error('❌ Error in tracking loop:', error);
|
);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
|
// Kiểm tra thời gian bid
|
||||||
await delay(configs.AUTO_TRACKING_DELAY);
|
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...");
|
||||||
|
clearLazyTab();
|
||||||
|
|
||||||
|
// Cập nhật trạng thái tracking
|
||||||
|
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.log(
|
||||||
|
`⏳ Waiting ${
|
||||||
|
configs.AUTO_TRACKING_DELAY / 1000
|
||||||
|
} seconds before the next iteration...`
|
||||||
|
);
|
||||||
|
await delay(configs.AUTO_TRACKING_DELAY);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearLazyTab = async () => {
|
const clearLazyTab = async () => {
|
||||||
if (!global.IS_CLEANING) {
|
if (!global.IS_CLEANING) {
|
||||||
console.log('🚀 Cleaning flag is OFF. Proceeding with operation.');
|
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!browser) {
|
if (!browser) {
|
||||||
console.warn('⚠️ Browser is not available or disconnected.');
|
console.warn("⚠️ Browser is not available or disconnected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = await browser.pages();
|
const pages = await browser.pages();
|
||||||
|
|
||||||
// Lấy danh sách URL từ flattenedArray
|
// 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
|
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(
|
console.log(
|
||||||
'🔍 Page URLs:',
|
"🔍 Page URLs:",
|
||||||
pages.map((page) => page.url()),
|
pages.map((page) => page.url())
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
const pageUrl = page.url();
|
const pageUrl = page.url();
|
||||||
|
|
||||||
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có 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)) {
|
if (!activeUrls.includes(pageUrl)) {
|
||||||
if (!page.isClosed() && browser.isConnected()) {
|
if (!page.isClosed() && browser.isConnected()) {
|
||||||
try {
|
try {
|
||||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||||
.map((i) => ({
|
.map((i) => ({
|
||||||
current_url: i.page_context.url(),
|
current_url: i.page_context.url(),
|
||||||
data: i,
|
data: i,
|
||||||
}))
|
}))
|
||||||
.find((j) => j.current_url === pageUrl);
|
.find((j) => j.current_url === pageUrl);
|
||||||
|
|
||||||
console.log(bidData);
|
console.log(bidData);
|
||||||
|
|
||||||
if (bidData && bidData.data) {
|
if (bidData && bidData.data) {
|
||||||
await safeClosePage(bidData.data);
|
await safeClosePage(bidData.data);
|
||||||
} else {
|
} else {
|
||||||
await page.close();
|
await page.close();
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Error in clearLazyTab:", err.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const workTracking = async () => {
|
const workTracking = async () => {
|
||||||
try {
|
try {
|
||||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
const activeData = _.flatMap(MANAGER_BIDS, (item) => [
|
||||||
const limit = pLimit(5);
|
item,
|
||||||
|
...item.children,
|
||||||
|
]);
|
||||||
|
const limit = pLimit(5);
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
activeData
|
activeData
|
||||||
.filter((item) => item.page_context && !item.page_context.isClosed())
|
.filter((item) => item.page_context && !item.page_context.isClosed())
|
||||||
.filter((item) => !activeTasks.has(item.id))
|
.filter((item) => !activeTasks.has(item.id))
|
||||||
.map((item) =>
|
.map((item) =>
|
||||||
limit(async () => {
|
limit(async () => {
|
||||||
activeTasks.add(item.id);
|
activeTasks.add(item.id);
|
||||||
try {
|
try {
|
||||||
await item.handleTakeWorkSnapshot();
|
await item.handleTakeWorkSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error);
|
console.error(
|
||||||
} finally {
|
`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`,
|
||||||
activeTasks.delete(item.id);
|
error
|
||||||
}
|
);
|
||||||
}),
|
} finally {
|
||||||
),
|
activeTasks.delete(item.id);
|
||||||
);
|
}
|
||||||
} catch (error) {
|
})
|
||||||
console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack);
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
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 () => {
|
(async () => {
|
||||||
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
||||||
transports: ['websocket'],
|
transports: ["websocket"],
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
extraHeaders: {
|
extraHeaders: {
|
||||||
Authorization: process.env.CLIENT_KEY,
|
Authorization: process.env.CLIENT_KEY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// set socket on global app
|
// set socket on global app
|
||||||
global.socket = socket;
|
global.socket = socket;
|
||||||
|
|
||||||
// listen connect
|
// listen connect
|
||||||
socket.on('connect', () => {
|
socket.on("connect", () => {
|
||||||
console.log('✅ Connected to WebSocket server');
|
console.log("✅ Connected to WebSocket server");
|
||||||
console.log('🔗 Socket ID:', socket.id);
|
console.log("🔗 Socket ID:", socket.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen connect
|
// listen connect
|
||||||
socket.on('disconnect', () => {
|
socket.on("disconnect", () => {
|
||||||
console.log('❌Client key is valid. Disconnected');
|
console.log("❌Client key is valid. Disconnected");
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen event
|
// listen event
|
||||||
socket.on('bidsUpdated', async (data) => {
|
socket.on("bidsUpdated", async (data) => {
|
||||||
console.log('📢 Bids Data:', data);
|
console.log("📢 Bids Data:", data);
|
||||||
|
|
||||||
handleUpdateProductTabs(data);
|
handleUpdateProductTabs(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('webUpdated', async (data) => {
|
socket.on("webUpdated", async (data) => {
|
||||||
console.log('📢 Account was updated:', data);
|
console.log("📢 Account was updated:", data);
|
||||||
|
|
||||||
const isDeleted = deleteProfile(data);
|
const isDeleted = deleteProfile(data);
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
console.log('✅ Profile deleted successfully!');
|
console.log("✅ Profile deleted successfully!");
|
||||||
|
|
||||||
const tab = MANAGER_BIDS.find((item) => item.url === data.url);
|
const tab = MANAGER_BIDS.find((item) => item.url === data.url);
|
||||||
|
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
global.IS_CLEANING = false;
|
global.IS_CLEANING = false;
|
||||||
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
|
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
|
||||||
|
|
||||||
await safeClosePage(tab);
|
await safeClosePage(tab);
|
||||||
|
|
||||||
global.IS_CLEANING = true;
|
global.IS_CLEANING = true;
|
||||||
} else {
|
} else {
|
||||||
console.log('⚠️ No profile found to delete.');
|
console.log("⚠️ No profile found to delete.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// AUTO TRACKING
|
// AUTO TRACKING
|
||||||
tracking();
|
tracking();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,44 @@
|
||||||
import BID_TYPE from '../system/bid-type.js';
|
import BID_TYPE from "../system/bid-type.js";
|
||||||
import CONSTANTS from '../system/constants.js';
|
import CONSTANTS from "../system/constants.js";
|
||||||
import { takeSnapshot } from '../system/utils.js';
|
import { takeSnapshot } from "../system/utils.js";
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
|
|
||||||
export class Bid {
|
export class Bid {
|
||||||
type;
|
type;
|
||||||
puppeteer_connect;
|
puppeteer_connect;
|
||||||
url;
|
url;
|
||||||
action;
|
action;
|
||||||
page_context;
|
page_context;
|
||||||
|
|
||||||
constructor(type, url, puppeteer_connect) {
|
constructor(type, url, puppeteer_connect) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.puppeteer_connect = puppeteer_connect;
|
this.puppeteer_connect = puppeteer_connect;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTakeWorkSnapshot = _.debounce(async () => {
|
||||||
|
if (!this.page_context) return;
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ Error taking snapshot for Product ID: ${this.id}:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
handleTakeWorkSnapshot = _.debounce(async () => {
|
async isLogin() {}
|
||||||
if (!this.page_context) return;
|
|
||||||
|
|
||||||
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);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,226 +1,292 @@
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
|
||||||
import configs from '../../system/config.js';
|
import configs from "../../system/config.js";
|
||||||
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
|
import {
|
||||||
import { ApiBid } from '../api-bid.js';
|
delay,
|
||||||
import fs from 'fs';
|
extractNumber,
|
||||||
|
getPathProfile,
|
||||||
|
isTimeReached,
|
||||||
|
safeClosePage,
|
||||||
|
} from "../../system/utils.js";
|
||||||
|
import { ApiBid } from "../api-bid.js";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
export class GrayApiBid extends ApiBid {
|
export class GrayApiBid extends ApiBid {
|
||||||
retry_login = 0;
|
retry_login = 0;
|
||||||
retry_login_count = 3;
|
retry_login_count = 3;
|
||||||
|
|
||||||
constructor({ ...prev }) {
|
constructor({ ...prev }) {
|
||||||
super(prev);
|
super(prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
async polling(page) {
|
async polling(page) {
|
||||||
try {
|
try {
|
||||||
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
||||||
// page.removeAllListeners('request');
|
// page.removeAllListeners('request');
|
||||||
// await page.setRequestInterception(true);
|
// await page.setRequestInterception(true);
|
||||||
|
|
||||||
// page.on('request', (request) => {
|
// page.on('request', (request) => {
|
||||||
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
|
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
|
||||||
// console.log('🚀 Fake response cho request:', request.url());
|
// console.log('🚀 Fake response cho request:', request.url());
|
||||||
|
|
||||||
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
|
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
|
||||||
|
|
||||||
// request.respond({
|
// request.respond({
|
||||||
// status: 200,
|
// status: 200,
|
||||||
// contentType: 'application/json',
|
// contentType: 'application/json',
|
||||||
// body: fakeData,
|
// body: fakeData,
|
||||||
// });
|
// });
|
||||||
// } else {
|
// } else {
|
||||||
// try {
|
// try {
|
||||||
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
|
|
||||||
console.log(`🔄 [${this.id}] Starting polling process...`);
|
console.log(`🔄 [${this.id}] Starting polling process...`);
|
||||||
|
|
||||||
await page.evaluateHandle(
|
await page.evaluateHandle(
|
||||||
(apiUrl, interval, bidId) => {
|
(apiUrl, interval, bidId) => {
|
||||||
if (window._autoBidPollingStarted) {
|
if (window._autoBidPollingStarted) {
|
||||||
console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`);
|
console.log(
|
||||||
return;
|
`✅ [${bidId}] Polling is already running. Skipping initialization.`
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🚀 [${bidId}] Initializing polling...`);
|
|
||||||
window._autoBidPollingStarted = true;
|
|
||||||
|
|
||||||
function sendRequest() {
|
|
||||||
console.log(`📡 [${bidId}] Sending request to track out-bid lots...`);
|
|
||||||
fetch(apiUrl, {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
window._pollingInterval = setInterval(sendRequest, interval);
|
|
||||||
},
|
|
||||||
configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
|
|
||||||
configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
|
|
||||||
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(() => {});
|
|
||||||
return await this.polling(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleCreateLogsOnServer(data) {
|
|
||||||
if (!Array.isArray(data)) return;
|
|
||||||
|
|
||||||
const values = data.map((item) => {
|
|
||||||
return {
|
|
||||||
model: item.Sku,
|
|
||||||
lot_id: item.Id,
|
|
||||||
out_price: extractNumber(item.Bid) || 0,
|
|
||||||
raw_data: JSON.stringify(item),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await createOutBidLog(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
listen_out_bids = async (data) => {
|
|
||||||
if (this.children.length <= 0 || data.length <= 0) return;
|
|
||||||
|
|
||||||
// SAVE LOGS ON SERVER
|
|
||||||
this.handleCreateLogsOnServer(data);
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
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...`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.children_processing.push(product_tab);
|
|
||||||
|
|
||||||
if (!product_tab.page_context) {
|
|
||||||
await product_tab.puppeteer_connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
await product_tab.action();
|
|
||||||
|
|
||||||
this.children_processing = this.children_processing.filter((item) => item.id !== product_tab.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async handleLogin() {
|
|
||||||
const page = this.page_context;
|
|
||||||
|
|
||||||
global.IS_CLEANING = false;
|
|
||||||
|
|
||||||
const filePath = getPathProfile(this.origin_url);
|
|
||||||
|
|
||||||
// 🔍 Check if already logged in (login input should not be visible)
|
|
||||||
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
|
|
||||||
console.log(`✅ [${this.id}] Already logged in, skipping login.`);
|
|
||||||
|
|
||||||
global.IS_CLEANING = true;
|
|
||||||
this.retry_login = 0; // Reset retry count
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
console.log(`🚀 [${bidId}] Initializing polling...`);
|
||||||
|
window._autoBidPollingStarted = true;
|
||||||
|
|
||||||
try {
|
function sendRequest() {
|
||||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
console.log(
|
||||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
`📡 [${bidId}] Sending request to track out-bid lots...`
|
||||||
await page.click('#loginButton');
|
);
|
||||||
|
fetch(apiUrl, {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.race([
|
window._pollingInterval = setInterval(sendRequest, interval);
|
||||||
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
|
},
|
||||||
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
|
configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
|
||||||
]);
|
configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
|
||||||
|
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(() => {});
|
||||||
|
return await this.polling(page);
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await page.$('input[name="username"]'))) {
|
console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
|
||||||
console.log(`✅ [${this.id}] Login successful!`);
|
throw error;
|
||||||
this.retry_login = 0; // Reset retry count after success
|
}
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Login failed, login input is still visible.');
|
async handleCreateLogsOnServer(data) {
|
||||||
} catch (error) {
|
if (!Array.isArray(data)) return;
|
||||||
console.log(`⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${this.retry_login + 1} ❌`);
|
|
||||||
|
|
||||||
this.retry_login++;
|
const values = data.map((item) => {
|
||||||
if (this.retry_login > this.retry_login_count) {
|
return {
|
||||||
console.log(`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`);
|
model: item.Sku,
|
||||||
safeClosePage(this);
|
lot_id: item.Id,
|
||||||
this.retry_login = 0; // Reset retry count
|
out_price: extractNumber(item.Bid) || 0,
|
||||||
return;
|
raw_data: JSON.stringify(item),
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
|
||||||
safeClosePage(this); // Close the current page
|
await createOutBidLog(values);
|
||||||
await delay(1000);
|
}
|
||||||
|
|
||||||
if (!this.page_context) {
|
listen_out_bids = async (data) => {
|
||||||
await this.puppeteer_connect(); // Reconnect if page is closed
|
if (this.children.length <= 0 || data.length <= 0) return;
|
||||||
}
|
|
||||||
|
|
||||||
return await this.action(); // Retry login
|
// SAVE LOGS ON SERVER
|
||||||
} finally {
|
this.handleCreateLogsOnServer(data);
|
||||||
global.IS_CLEANING = true;
|
|
||||||
}
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
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...`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.children_processing.push(product_tab);
|
||||||
|
|
||||||
|
if (!product_tab.page_context) {
|
||||||
|
await product_tab.puppeteer_connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
await product_tab.action();
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
action = async () => {
|
return false;
|
||||||
try {
|
};
|
||||||
const page = this.page_context;
|
|
||||||
|
|
||||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
async handleLogin() {
|
||||||
console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
|
const page = this.page_context;
|
||||||
|
|
||||||
await page.bringToFront();
|
global.IS_CLEANING = false;
|
||||||
console.log(`🎯 [${this.id}] Brought page to front.`);
|
|
||||||
|
|
||||||
// Set userAgent
|
const filePath = getPathProfile(this.origin_url);
|
||||||
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) => {
|
// 🔍 Check if already logged in (login input should not be visible)
|
||||||
if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
|
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
|
||||||
console.log(`🚀 [${this.id}] API POST detected: ${response.url()}`);
|
console.log(`✅ [${this.id}] Already logged in, skipping login.`);
|
||||||
|
|
||||||
try {
|
global.IS_CLEANING = true;
|
||||||
const responseBody = await response.json();
|
this.retry_login = 0; // Reset retry count
|
||||||
await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
|
return;
|
||||||
} catch (error) {
|
}
|
||||||
console.error(`❌ [${this.id}] Error processing response:`, error?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
page.on('load', async () => {
|
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||||
console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
|
|
||||||
await this.polling(page);
|
|
||||||
await this.handleLogin();
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.polling(page); // Call when first load
|
try {
|
||||||
await this.handleLogin();
|
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||||
} catch (error) {
|
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||||
console.log(`❌ [${this.id}] Action error: ${error.message}`);
|
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
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!(await page.$('input[name="username"]'))) {
|
||||||
|
console.log(`✅ [${this.id}] Login successful!`);
|
||||||
|
this.retry_login = 0; // Reset retry count after success
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
} ❌`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.retry_login++;
|
||||||
|
if (this.retry_login > this.retry_login_count) {
|
||||||
|
console.log(
|
||||||
|
`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`
|
||||||
|
);
|
||||||
|
safeClosePage(this);
|
||||||
|
this.retry_login = 0; // Reset retry count
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
safeClosePage(this); // Close the current page
|
||||||
|
await delay(1000);
|
||||||
|
|
||||||
|
if (!this.page_context) {
|
||||||
|
await this.puppeteer_connect(); // Reconnect if page is closed
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.action(); // Retry login
|
||||||
|
} finally {
|
||||||
|
global.IS_CLEANING = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action = async () => {
|
||||||
|
try {
|
||||||
|
const page = this.page_context;
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
console.log(`🛠️ [${this.id}] UserAgent set.`);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
page.on("load", async () => {
|
||||||
|
console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
|
||||||
|
await this.polling(page);
|
||||||
|
await this.handleLogin();
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.polling(page); // Call when first load
|
||||||
|
await this.handleLogin();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌ [${this.id}] Action error: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,245 +1,322 @@
|
||||||
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
|
import {
|
||||||
import CONSTANTS from '../../system/constants.js';
|
outBid,
|
||||||
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
pushPrice,
|
||||||
import { ProductBid } from '../product-bid.js';
|
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 {
|
export class GraysProductBid extends ProductBid {
|
||||||
constructor({ ...prev }) {
|
constructor({ ...prev }) {
|
||||||
super(prev);
|
super(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate({ page, price_value }) {
|
||||||
|
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
|
||||||
|
console.log(`❌ [${this.id}] It's not time yet`);
|
||||||
|
return { result: false, bid_price: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate({ page, price_value }) {
|
if (!isNumber(price_value)) {
|
||||||
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
|
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
|
||||||
console.log(`❌ [${this.id}] It's not time yet`);
|
await takeSnapshot(page, this, "price-value-null");
|
||||||
return { result: false, bid_price: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNumber(price_value)) {
|
return { result: false, bid_price: 0 };
|
||||||
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
|
|
||||||
await takeSnapshot(page, this, 'price-value-null');
|
|
||||||
|
|
||||||
return { result: false, bid_price: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
await outBid(this.id);
|
|
||||||
|
|
||||||
return { result: false, bid_price: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await pushPrice({
|
|
||||||
bid_id: this.id,
|
|
||||||
price: bid_price,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.status) {
|
|
||||||
return { result: false, bid_price: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.histories = response.data;
|
|
||||||
|
|
||||||
// RESET first bid
|
|
||||||
if (this.histories.length > 0 && this.first_bid) {
|
|
||||||
this.first_bid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { result: true, bid_price };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCloseTime = async () => {
|
const bid_price = this.plus_price + Number(price_value);
|
||||||
try {
|
|
||||||
if (!this.page_context) return null;
|
|
||||||
|
|
||||||
await this.page_context.waitForSelector('#lot-closing-datetime', { timeout: 3000 });
|
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");
|
||||||
|
|
||||||
return await this.page_context.$eval('#lot-closing-datetime', (el) => el.value);
|
await outBid(this.id);
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getPriceWasBid = async () => {
|
return { result: false, bid_price: 0 };
|
||||||
try {
|
|
||||||
if (!this.page_context) return null;
|
|
||||||
|
|
||||||
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 textPrice = await this.page_context.evaluate((el) => el.textContent, element);
|
|
||||||
|
|
||||||
return extractNumber(textPrice) || null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async isCloseProduct() {
|
|
||||||
const close_time = await this.getCloseTime();
|
|
||||||
|
|
||||||
if (!close_time) {
|
|
||||||
const priceWasBid = await this.getPriceWasBid();
|
|
||||||
|
|
||||||
await updateStatusByPrice(this.id, priceWasBid);
|
|
||||||
return { result: true, close_time: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
|
|
||||||
console.log(`❌ [${this.id}] Product is close ${close_time}`);
|
|
||||||
return { result: true, close_time };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { result: false, close_time };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleWritePrice(page, bid_price) {
|
const response = await pushPrice({
|
||||||
await page.type('#price', String(bid_price));
|
bid_id: this.id,
|
||||||
await delay(500);
|
price: bid_price,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.status) {
|
||||||
|
return { result: false, bid_price: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async placeBid(page) {
|
this.histories = response.data;
|
||||||
try {
|
|
||||||
await page.click('#bid-type-standard');
|
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
await page.click('#btnSubmit');
|
// RESET first bid
|
||||||
await delay(1000);
|
if (this.histories.length > 0 && this.first_bid) {
|
||||||
|
this.first_bid = false;
|
||||||
await page.waitForSelector('button', { timeout: 5000 });
|
|
||||||
|
|
||||||
await delay(500);
|
|
||||||
|
|
||||||
await page.click('button');
|
|
||||||
|
|
||||||
await page.waitForNavigation({ timeout: 5000 });
|
|
||||||
|
|
||||||
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');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleReturnProductPage(page) {
|
return { result: true, bid_price };
|
||||||
await page.goto(this.url);
|
}
|
||||||
await delay(1000);
|
|
||||||
|
getCloseTime = async () => {
|
||||||
|
try {
|
||||||
|
if (!this.page_context) return null;
|
||||||
|
|
||||||
|
await this.page_context.waitForSelector("#lot-closing-datetime", {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.page_context.$eval(
|
||||||
|
"#lot-closing-datetime",
|
||||||
|
(el) => el.value
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getPriceWasBid = async () => {
|
||||||
|
try {
|
||||||
|
if (!this.page_context) return null;
|
||||||
|
|
||||||
|
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 textPrice = await this.page_context.evaluate(
|
||||||
|
(el) => el.textContent,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
|
||||||
|
return extractNumber(textPrice) || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async isCloseProduct() {
|
||||||
|
const close_time = await this.getCloseTime();
|
||||||
|
|
||||||
|
if (!close_time) {
|
||||||
|
const priceWasBid = await this.getPriceWasBid();
|
||||||
|
|
||||||
|
await updateStatusByPrice(this.id, priceWasBid);
|
||||||
|
return { result: true, close_time: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
await delay(500);
|
||||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
|
||||||
|
|
||||||
if (response) {
|
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
|
||||||
this.lot_id = response.lot_id;
|
console.log(`❌ [${this.id}] Product is close ${close_time}`);
|
||||||
this.close_time = response.close_time;
|
return { result: true, close_time };
|
||||||
this.start_bid_time = response.start_bid_time;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update = async () => {
|
return { result: false, close_time };
|
||||||
if (!this.page_context) return;
|
}
|
||||||
|
|
||||||
const page = this.page_context;
|
async handleWritePrice(page, bid_price) {
|
||||||
|
await page.type("#price", String(bid_price));
|
||||||
|
await delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
async placeBid(page) {
|
||||||
const close_time = await this.getCloseTime();
|
try {
|
||||||
|
await page.click("#bid-type-standard");
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
// Chờ phần tử xuất hiện trước khi lấy giá trị
|
await page.click("#btnSubmit");
|
||||||
await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
|
await delay(1000);
|
||||||
const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
|
|
||||||
|
|
||||||
await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
|
await page.waitForSelector("button", { timeout: 5000 });
|
||||||
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);
|
await delay(500);
|
||||||
const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
|
|
||||||
|
|
||||||
await page
|
await page.click("button");
|
||||||
.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}`);
|
await page.waitForNavigation({ timeout: 5000 });
|
||||||
|
|
||||||
const data = removeFalsyValues(
|
await takeSnapshot(
|
||||||
{
|
page,
|
||||||
lot_id,
|
this,
|
||||||
reserve_price: price_value,
|
"bid-success",
|
||||||
close_time: close_time ? String(close_time) : null,
|
CONSTANTS.TYPE_IMAGE.SUCCESS
|
||||||
name,
|
);
|
||||||
current_price: current_price ? extractNumber(current_price) : null,
|
return true;
|
||||||
},
|
} catch (error) {
|
||||||
['close_time'],
|
console.log(`❌ [${this.id}] Timeout to loading`);
|
||||||
);
|
await takeSnapshot(page, this, "timeout to loading");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.handleUpdateBid(data);
|
async handleReturnProductPage(page) {
|
||||||
|
await page.goto(this.url);
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
return { price_value, lot_id, name, current_price };
|
async handleUpdateBid({
|
||||||
} catch (error) {
|
lot_id,
|
||||||
console.error(`🚨 Error updating product info: ${error.message}`);
|
close_time,
|
||||||
return null;
|
name,
|
||||||
}
|
current_price,
|
||||||
};
|
reserve_price,
|
||||||
|
}) {
|
||||||
|
const response = await updateBid(this.id, {
|
||||||
|
lot_id,
|
||||||
|
close_time,
|
||||||
|
name,
|
||||||
|
current_price,
|
||||||
|
reserve_price: Number(reserve_price) || 0,
|
||||||
|
});
|
||||||
|
|
||||||
action = async () => {
|
if (response) {
|
||||||
try {
|
this.lot_id = response.lot_id;
|
||||||
const page = this.page_context;
|
this.close_time = response.close_time;
|
||||||
|
this.start_bid_time = response.start_bid_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.gotoLink();
|
update = async () => {
|
||||||
console.log(`🌍 [${this.id}] Navigated to link.`);
|
if (!this.page_context) return;
|
||||||
|
|
||||||
await delay(1000);
|
const page = this.page_context;
|
||||||
|
|
||||||
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
|
try {
|
||||||
if (isCloseProduct.result) {
|
const close_time = await this.getCloseTime();
|
||||||
console.log(`❌ [${this.id}] The product is closed, cannot place a bid.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay(500);
|
// 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);
|
||||||
|
|
||||||
const { price_value } = await this.update();
|
await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
|
||||||
if (!price_value) return;
|
const lot_id = await page
|
||||||
|
.$eval("#lotId", (el) => el.value)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
const { result, bid_price } = await this.validate({ page, price_value });
|
await page
|
||||||
if (!result) {
|
.waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
|
||||||
console.log(`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`);
|
timeout: 5000,
|
||||||
return;
|
})
|
||||||
}
|
.catch(() => null);
|
||||||
|
const name = await page
|
||||||
|
.$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
const bidHistoriesItem = _.maxBy(this.histories, 'price');
|
await page
|
||||||
if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
|
.waitForSelector(
|
||||||
console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
|
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
|
||||||
return;
|
{ 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);
|
||||||
|
|
||||||
if (price_value != bid_price) {
|
console.log(
|
||||||
console.log(`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`);
|
`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
|
||||||
await this.handleWritePrice(page, bid_price);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🚀 [${this.id}] Placing the bid...`);
|
const data = removeFalsyValues(
|
||||||
const resultPlaceBid = await this.placeBid(page);
|
{
|
||||||
if (!resultPlaceBid) {
|
lot_id,
|
||||||
console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
|
reserve_price: price_value,
|
||||||
await takeSnapshot(page, this, 'place-bid-action');
|
close_time: close_time ? String(close_time) : null,
|
||||||
return;
|
name,
|
||||||
}
|
current_price: current_price ? extractNumber(current_price) : null,
|
||||||
|
},
|
||||||
|
["close_time"]
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`);
|
this.handleUpdateBid(data);
|
||||||
await this.handleReturnProductPage(page);
|
|
||||||
} catch (error) {
|
return { price_value, lot_id, name, current_price };
|
||||||
console.error(`🚨 [${this.id}] Error navigating the page: ${error.message}`);
|
} catch (error) {
|
||||||
}
|
console.error(`🚨 Error updating product info: ${error.message}`);
|
||||||
};
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
action = async () => {
|
||||||
|
try {
|
||||||
|
const page = this.page_context;
|
||||||
|
|
||||||
|
await this.gotoLink();
|
||||||
|
console.log(`🌍 [${this.id}] Navigated to link.`);
|
||||||
|
|
||||||
|
await delay(1000);
|
||||||
|
|
||||||
|
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
|
||||||
|
if (isCloseProduct.result) {
|
||||||
|
console.log(
|
||||||
|
`❌ [${this.id}] The product is closed, cannot place a bid.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
const { price_value } = await this.update();
|
||||||
|
if (!price_value) return;
|
||||||
|
|
||||||
|
const { result, bid_price } = await this.validate({ page, price_value });
|
||||||
|
if (!result) {
|
||||||
|
console.log(
|
||||||
|
`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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})`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price_value != bid_price) {
|
||||||
|
console.log(
|
||||||
|
`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`
|
||||||
|
);
|
||||||
|
await this.handleWritePrice(page, bid_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 [${this.id}] Placing the bid...`);
|
||||||
|
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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,227 +1,284 @@
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
import configs from '../../system/config.js';
|
import configs from "../../system/config.js";
|
||||||
import { getPathProfile, safeClosePage } from '../../system/utils.js';
|
import { getPathProfile, safeClosePage } from "../../system/utils.js";
|
||||||
import { ApiBid } from '../api-bid.js';
|
import { ApiBid } from "../api-bid.js";
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
import { updateStatusByPrice } from '../../system/apis/bid.js';
|
import { updateStatusByPrice } from "../../system/apis/bid.js";
|
||||||
|
|
||||||
export class LangtonsApiBid extends ApiBid {
|
export class LangtonsApiBid extends ApiBid {
|
||||||
reloadInterval = null;
|
reloadInterval = null;
|
||||||
constructor({ ...prev }) {
|
constructor({ ...prev }) {
|
||||||
super(prev);
|
super(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
waitVerifyData = async () =>
|
||||||
|
new Promise((rev, rej) => {
|
||||||
|
// 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.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 120 * 1000); // 120 giây
|
||||||
|
|
||||||
|
global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
|
||||||
|
console.log(`📢 [${this.id}] VERIFY CODE:`, data);
|
||||||
|
clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
|
||||||
|
global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
|
||||||
|
rev(data); // Resolve với dữ liệu nhận được
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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.$('input[name="loginEmail"]')) &&
|
||||||
|
fs.existsSync(filePath)
|
||||||
|
) {
|
||||||
|
console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
waitVerifyData = async () =>
|
if (fs.existsSync(filePath)) {
|
||||||
new Promise((rev, rej) => {
|
console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
|
||||||
// Tạo timeout để reject sau 1 phút nếu không có phản hồi
|
fs.unlinkSync(filePath);
|
||||||
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.`));
|
|
||||||
}, 120 * 1000); // 120 giây
|
|
||||||
|
|
||||||
global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
|
|
||||||
console.log(`📢 [${this.id}] VERIFY CODE:`, data);
|
|
||||||
clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
|
|
||||||
global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
|
|
||||||
rev(data); // Resolve với dữ liệu nhận được
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async isLogin() {
|
|
||||||
if (!this.page_context) return false;
|
|
||||||
|
|
||||||
const filePath = getPathProfile(this.origin_url);
|
|
||||||
|
|
||||||
return !(await this.page_context.$('input[name="loginEmail"]')) && fs.existsSync(filePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLogin() {
|
const children = this.children.filter((item) => item.page_context);
|
||||||
const page = this.page_context;
|
console.log(
|
||||||
|
`🔍 [${this.id}] Found ${children.length} child pages to close.`
|
||||||
|
);
|
||||||
|
|
||||||
global.IS_CLEANING = false;
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const filePath = getPathProfile(this.origin_url);
|
console.log(
|
||||||
|
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
||||||
await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
|
);
|
||||||
|
await safeClosePage(this);
|
||||||
// 🛠 Check if already logged in (login input should not be visible or profile exists)
|
|
||||||
if (!(await page.$('input[name="loginEmail"]')) && 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('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 });
|
|
||||||
|
|
||||||
// ✅ Click the "Remember Me" checkbox
|
|
||||||
console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
|
|
||||||
await page.click('#rememberMe', { delay: 80 });
|
|
||||||
|
|
||||||
// 🚀 Click the login button
|
|
||||||
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
|
|
||||||
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' });
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
|
|
||||||
// ⏳ Wait for verification code from socket event
|
|
||||||
const { name, code } = await this.waitVerifyData();
|
|
||||||
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 });
|
|
||||||
|
|
||||||
// 🚀 Click the verification confirmation button
|
|
||||||
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' });
|
|
||||||
|
|
||||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
|
||||||
|
|
||||||
// 📂 Save session context to avoid re-login
|
|
||||||
await this.saveContext();
|
|
||||||
console.log(`✅ [${this.id}] Login successful!`);
|
|
||||||
|
|
||||||
// await page.goto(this.url);
|
|
||||||
console.log(`✅ [${this.id}] Navigation successful!`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ [${this.id}] Error during login process:`, error.message);
|
|
||||||
} finally {
|
|
||||||
global.IS_CLEANING = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWonList() {
|
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||||
try {
|
|
||||||
await page.waitForSelector('.row.account-product-list', { timeout: 30000 });
|
|
||||||
|
|
||||||
const items = await page.evaluate(() => {
|
try {
|
||||||
return Array.from(document.querySelectorAll('.row.account-product-list')).map((item) => item.getAttribute('data-lotid') || null);
|
// ⌨ Enter email
|
||||||
});
|
console.log(`✍ [${this.id}] Entering email:`, this.username);
|
||||||
|
await page.type('input[name="loginEmail"]', this.username, {
|
||||||
|
delay: 100,
|
||||||
|
});
|
||||||
|
|
||||||
return _.compact(items);
|
// ⌨ Enter password
|
||||||
} catch (error) {
|
console.log(`✍ [${this.id}] Entering password...`);
|
||||||
return [];
|
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 });
|
||||||
|
|
||||||
|
// 🚀 Click the login button
|
||||||
|
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// ⏳ Wait for verification code from socket event
|
||||||
|
const { name, code } = await this.waitVerifyData();
|
||||||
|
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 });
|
||||||
|
|
||||||
|
// 🚀 Click the verification confirmation button
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||||
|
|
||||||
|
// 📂 Save session context to avoid re-login
|
||||||
|
await this.saveContext();
|
||||||
|
console.log(`✅ [${this.id}] Login successful!`);
|
||||||
|
|
||||||
|
// await page.goto(this.url);
|
||||||
|
console.log(`✅ [${this.id}] Navigation successful!`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ [${this.id}] Error during login process:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
global.IS_CLEANING = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWonList() {
|
||||||
|
try {
|
||||||
|
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 _.compact(items);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUpdateWonItem() {
|
||||||
|
console.log(`🔄 [${this.id}] Starting to update the won list...`);
|
||||||
|
|
||||||
|
// Lấy danh sách các lot_id thắng
|
||||||
|
const items = await this.getWonList();
|
||||||
|
console.log(`📌 [${this.id}] List of won lot_ids:`, items);
|
||||||
|
|
||||||
|
// Nếu không có item nào, thoát ra
|
||||||
|
if (items.length === 0) {
|
||||||
|
console.log(`⚠️ [${this.id}] No items to update.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUpdateWonItem() {
|
// Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
|
||||||
console.log(`🔄 [${this.id}] Starting to update the won list...`);
|
const result = _.filter(this.children, (item) =>
|
||||||
|
_.includes(items, item.lot_id)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`✅ [${this.id}] ${result.length} items need to be updated:`,
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
// Lấy danh sách các lot_id thắng
|
// Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
|
||||||
const items = await this.getWonList();
|
const responses = await Promise.allSettled(
|
||||||
console.log(`📌 [${this.id}] List of won lot_ids:`, items);
|
result.map((i) => updateStatusByPrice(i.id, i.current_price))
|
||||||
|
);
|
||||||
|
|
||||||
// Nếu không có item nào, thoát ra
|
// Log kết quả của mỗi request
|
||||||
if (items.length === 0) {
|
responses.forEach((response, index) => {
|
||||||
console.log(`⚠️ [${this.id}] No items to update.`);
|
if (response.status === "fulfilled") {
|
||||||
return;
|
console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`❌ [${this.id}] Update failed:`,
|
||||||
|
result[index],
|
||||||
|
response.reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🏁 [${this.id}] Finished updating the won list.`);
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.LANGTONS.LOGIN_URL)) {
|
||||||
|
await this.handleLogin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
|
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||||
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
|
await page.bringToFront();
|
||||||
const responses = await Promise.allSettled(result.map((i) => updateStatusByPrice(i.id, i.current_price)));
|
|
||||||
|
|
||||||
// Log kết quả của mỗi request
|
// Set userAgent
|
||||||
responses.forEach((response, index) => {
|
await page.setUserAgent(
|
||||||
if (response.status === 'fulfilled') {
|
"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}] Successfully updated:`, result[index]);
|
);
|
||||||
} else {
|
} catch (error) {
|
||||||
console.error(`❌ [${this.id}] Update failed:`, result[index], response.reason);
|
console.log("Error [action]: ", error.message);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`🏁 [${this.id}] Finished updating the won list.`);
|
|
||||||
return responses;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
action = async () => {
|
listen_events = async () => {
|
||||||
try {
|
if (this.page_context) return;
|
||||||
const page = this.page_context;
|
|
||||||
|
|
||||||
page.on('response', async (response) => {
|
await this.puppeteer_connect();
|
||||||
const request = response.request();
|
await this.action();
|
||||||
if (request.redirectChain().length > 0) {
|
|
||||||
if (response.url().includes(configs.WEB_CONFIGS.LANGTONS.LOGIN_URL)) {
|
|
||||||
await this.handleLogin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
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.`);
|
||||||
|
|
||||||
await page.bringToFront();
|
// this.handleUpdateWonItem();
|
||||||
|
} else {
|
||||||
// Set userAgent
|
console.log(
|
||||||
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');
|
`❌ [${this.id}] Page context is closed. Stopping reload.`
|
||||||
} catch (error) {
|
);
|
||||||
console.log('Error [action]: ', error.message);
|
clearInterval(this.reloadInterval);
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
|
||||||
listen_events = async () => {
|
}
|
||||||
if (this.page_context) return;
|
}, 60000); // 1p reload
|
||||||
|
};
|
||||||
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.`);
|
|
||||||
|
|
||||||
// this.handleUpdateWonItem();
|
|
||||||
} 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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,409 +1,510 @@
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
import { outBid, pushPrice, updateBid } from '../../system/apis/bid.js';
|
import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
|
||||||
import { sendMessage } from '../../system/apis/notification.js';
|
import { sendMessage } from "../../system/apis/notification.js";
|
||||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
|
||||||
import configs from '../../system/config.js';
|
import configs from "../../system/config.js";
|
||||||
import CONSTANTS from '../../system/constants.js';
|
import CONSTANTS from "../../system/constants.js";
|
||||||
import { convertAETtoUTC, isTimeReached, removeFalsyValues, takeSnapshot } from '../../system/utils.js';
|
import {
|
||||||
import { ProductBid } from '../product-bid.js';
|
convertAETtoUTC,
|
||||||
|
isTimeReached,
|
||||||
|
removeFalsyValues,
|
||||||
|
takeSnapshot,
|
||||||
|
} from "../../system/utils.js";
|
||||||
|
import { ProductBid } from "../product-bid.js";
|
||||||
|
|
||||||
export class LangtonsProductBid extends ProductBid {
|
export class LangtonsProductBid extends ProductBid {
|
||||||
constructor({ ...prev }) {
|
constructor({ ...prev }) {
|
||||||
super(prev);
|
super(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hàm lấy thời gian kết thúc từ trang web
|
||||||
|
async getCloseTime() {
|
||||||
|
try {
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
const time = await this.page_context.evaluate(() => {
|
||||||
|
const el = document.querySelector(".site-timezone");
|
||||||
|
return el ? el.innerText : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return time ? convertAETtoUTC(time) : null;
|
||||||
|
|
||||||
|
// return new Date(Date.now() + 6 * 60 * 1000).toUTCString();
|
||||||
|
} catch (error) {
|
||||||
|
// Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForApiResponse(timeout = 15000) {
|
||||||
|
if (!this.page_context) {
|
||||||
|
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hàm lấy thời gian kết thúc từ trang web
|
return new Promise((resolve) => {
|
||||||
async getCloseTime() {
|
const onResponse = async (response) => {
|
||||||
try {
|
try {
|
||||||
// Kiểm tra xem có context của trang web không, nếu không thì trả về null
|
if (
|
||||||
if (!this.page_context) return null;
|
!response ||
|
||||||
|
!response
|
||||||
await this.page_context.waitForSelector('.site-timezone', { timeout: 2000 });
|
.request()
|
||||||
const time = await this.page_context.evaluate(() => {
|
.url()
|
||||||
const el = document.querySelector('.site-timezone');
|
.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
|
||||||
return el ? el.innerText : null;
|
) {
|
||||||
});
|
|
||||||
|
|
||||||
return time ? convertAETtoUTC(time) : null;
|
|
||||||
|
|
||||||
// return new Date(Date.now() + 6 * 60 * 1000).toUTCString();
|
|
||||||
} catch (error) {
|
|
||||||
// Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForApiResponse(timeout = 15000) {
|
|
||||||
if (!this.page_context) {
|
|
||||||
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const onResponse = async (response) => {
|
|
||||||
try {
|
|
||||||
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
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
resolve(data);
|
|
||||||
} catch (error) {
|
|
||||||
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
|
|
||||||
|
|
||||||
await this.page_context.reload({ waitUntil: 'networkidle0' }); // reload page
|
|
||||||
|
|
||||||
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
|
|
||||||
resolve(null);
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
this.page_context.on('response', onResponse);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getName() {
|
|
||||||
try {
|
|
||||||
if (!this.page_context) return null;
|
|
||||||
|
|
||||||
await this.page_context.waitForSelector('.product-name', { timeout: 3000 });
|
|
||||||
|
|
||||||
return await this.page_context.evaluate(() => {
|
|
||||||
const el = document.querySelector('.product-name');
|
|
||||||
return el ? el.innerText : null;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
this.close_time = response.close_time;
|
|
||||||
this.start_bid_time = response.start_bid_time;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update = async () => {
|
|
||||||
if (!this.page_context) return;
|
|
||||||
|
|
||||||
console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
|
|
||||||
|
|
||||||
// 📌 Lấy thời gian kết thúc đấu giá từ giao diện
|
|
||||||
const close_time = await this.getCloseTime();
|
|
||||||
console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`);
|
|
||||||
|
|
||||||
// 📌 Lấy tên sản phẩm hoặc thông tin liên quan
|
|
||||||
const name = await this.getName();
|
|
||||||
console.log(`📌 [${this.id}] Retrieved name: ${name}`);
|
|
||||||
|
|
||||||
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
|
|
||||||
const result = await this.waitForApiResponse();
|
|
||||||
|
|
||||||
// 📌 Nếu không có dữ liệu trả về thì dừng
|
|
||||||
if (!result) {
|
|
||||||
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
|
clearTimeout(timer); // Hủy timeout nếu có phản hồi
|
||||||
const data = removeFalsyValues(
|
this.page_context.off("response", onResponse); // Gỡ bỏ listener
|
||||||
{
|
|
||||||
model: result?.pid || null,
|
const data = await response.json();
|
||||||
lot_id: result?.lotId || null,
|
resolve(data);
|
||||||
reserve_price: 21, //test
|
} catch (error) {
|
||||||
// reserve_price: result.lotData?.minimumBid || null,
|
console.error(
|
||||||
// current_price: result.lotData?.currentMaxBid || null,
|
`❌ [${this.id}] Error while parsing response:`,
|
||||||
current_price: 20, // test
|
error?.message
|
||||||
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
);
|
||||||
close_time: close_time ? String(close_time) : null,
|
resolve(null);
|
||||||
name,
|
}
|
||||||
},
|
};
|
||||||
// [],
|
|
||||||
['close_time'],
|
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
|
||||||
|
|
||||||
|
await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page
|
||||||
|
|
||||||
|
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
|
||||||
|
resolve(null);
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
this.page_context.on("response", onResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getName() {
|
||||||
|
try {
|
||||||
|
if (!this.page_context) return null;
|
||||||
|
|
||||||
|
await this.page_context.waitForSelector(".product-name", {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.page_context.evaluate(() => {
|
||||||
|
const el = document.querySelector(".product-name");
|
||||||
|
return el ? el.innerText : null;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
this.close_time = response.close_time;
|
||||||
|
this.start_bid_time = response.start_bid_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update = async () => {
|
||||||
|
if (!this.page_context) return;
|
||||||
|
|
||||||
|
console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
|
||||||
|
|
||||||
|
// 📌 Lấy thời gian kết thúc đấu giá từ giao diện
|
||||||
|
const close_time = await this.getCloseTime();
|
||||||
|
console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`);
|
||||||
|
|
||||||
|
// 📌 Lấy tên sản phẩm hoặc thông tin liên quan
|
||||||
|
const name = await this.getName();
|
||||||
|
console.log(`📌 [${this.id}] Retrieved name: ${name}`);
|
||||||
|
|
||||||
|
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
|
||||||
|
const result = await this.waitForApiResponse();
|
||||||
|
|
||||||
|
// 📌 Nếu không có dữ liệu trả về thì dừng
|
||||||
|
if (!result) {
|
||||||
|
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📌 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(
|
||||||
|
{
|
||||||
|
model: result?.pid || null,
|
||||||
|
lot_id: result?.lotId || null,
|
||||||
|
reserve_price: result.lotData?.minimumBid || null,
|
||||||
|
current_price: result.lotData?.currentMaxBid || null,
|
||||||
|
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
||||||
|
close_time: close_time ? String(close_time) : null,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
// [],
|
||||||
|
["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);
|
||||||
|
|
||||||
|
console.log("✅ Update successful!");
|
||||||
|
|
||||||
|
return { ...response, name, close_time };
|
||||||
|
};
|
||||||
|
|
||||||
|
async getContinueShopButton() {
|
||||||
|
try {
|
||||||
|
if (!this.page_context) return null;
|
||||||
|
|
||||||
|
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"
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`🚀 [${this.id}] Processed data ready for update`);
|
return el;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 📌 Gửi dữ liệu cập nhật lên hệ thống
|
async handlePlaceBid() {
|
||||||
await this.handleUpdateBid(data);
|
if (!this.page_context) {
|
||||||
|
console.log(
|
||||||
|
`⚠️ [${this.id}] No page context found, aborting bid process.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const page = this.page_context;
|
||||||
|
|
||||||
console.log('✅ Update successful!');
|
if (global[`IS_PLACE_BID-${this.id}`]) {
|
||||||
|
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
||||||
return { ...response, name, close_time };
|
return;
|
||||||
};
|
|
||||||
|
|
||||||
async getContinueShopButton() {
|
|
||||||
try {
|
|
||||||
if (!this.page_context) return null;
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
return el;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePlaceBid() {
|
try {
|
||||||
if (!this.page_context) {
|
console.log(`🔄 [${this.id}] Starting bid process...`);
|
||||||
console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
|
global[`IS_PLACE_BID-${this.id}`] = true;
|
||||||
|
|
||||||
|
const continueShopBtn = await this.getContinueShopButton();
|
||||||
|
if (continueShopBtn) {
|
||||||
|
console.log(
|
||||||
|
`⚠️ [${this.id}] Outbid detected, calling outBid function.`
|
||||||
|
);
|
||||||
|
await outBid(this.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
|
||||||
|
if (this.current_price > this.max_price + this.plus_price) {
|
||||||
|
console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
|
||||||
|
return; // Dừng hàm nếu giá đã vượt qua giới hạn
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đợi phản hồi từ API
|
||||||
|
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
|
||||||
|
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'
|
||||||
|
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");
|
||||||
|
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})`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
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 });
|
||||||
|
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
|
||||||
|
);
|
||||||
|
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
|
||||||
|
if (!bidValue || bidValue !== String(this.max_price)) {
|
||||||
|
console.log(`❌ Incorrect bid amount! Received: ${bidValue}`);
|
||||||
|
return; // Dừng thực hiện nếu giá trị nhập sai
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📌 Nhấn nút "Place Bid"
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
|
||||||
|
|
||||||
|
const { lotData } = await this.waitForApiResponse();
|
||||||
|
console.log(`📡 [${this.id}] API Response received:`, lotData);
|
||||||
|
|
||||||
|
// 📌 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
|
||||||
|
);
|
||||||
|
|
||||||
|
sendMessage(this);
|
||||||
|
|
||||||
|
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
||||||
|
global[`IS_PLACE_BID-${this.id}`] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateLogsOnServer(data) {
|
||||||
|
const values = data.map((item) => {
|
||||||
|
return {
|
||||||
|
model: item.pid,
|
||||||
|
lot_id: item.lotId,
|
||||||
|
out_price: item.lotData.minimumBid || 0,
|
||||||
|
raw_data: JSON.stringify(item),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOutBidLog(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
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...`);
|
||||||
|
|
||||||
|
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
||||||
|
// page.removeAllListeners('request');
|
||||||
|
|
||||||
|
// await page.setRequestInterception(true);
|
||||||
|
|
||||||
|
// page.on('request', (request) => {
|
||||||
|
// if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
|
||||||
|
// console.log('🚀 Fake response cho request:', request.url());
|
||||||
|
|
||||||
|
// const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8');
|
||||||
|
|
||||||
|
// request.respond({
|
||||||
|
// status: 200,
|
||||||
|
// contentType: 'application/json',
|
||||||
|
// body: fakeData,
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// try {
|
||||||
|
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
const onResponse = async (response) => {
|
||||||
|
const url = response?.request()?.url();
|
||||||
|
if (
|
||||||
|
!url ||
|
||||||
|
!url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { lotData, ...prev } = await response.json();
|
||||||
|
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}] Reload page in gotoLink`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const page = this.page_context;
|
|
||||||
|
|
||||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
console.log(`🔍 [${this.id}] Checking bid status...`);
|
||||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (["Outbid"].includes(lotData?.bidStatus)) {
|
||||||
console.log(`🔄 [${this.id}] Starting bid process...`);
|
console.log(
|
||||||
global[`IS_PLACE_BID-${this.id}`] = true;
|
`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`
|
||||||
|
);
|
||||||
|
|
||||||
const continueShopBtn = await this.getContinueShopButton();
|
this.handleCreateLogsOnServer([{ lotData, ...prev }]);
|
||||||
if (continueShopBtn) {
|
} else if (["Winning"].includes(lotData?.bidStatus)) {
|
||||||
console.log(`⚠️ [${this.id}] Outbid detected, calling outBid function.`);
|
const bidHistoriesItem = _.maxBy(this.histories, "price");
|
||||||
await outBid(this.id);
|
|
||||||
return;
|
if (
|
||||||
|
!bidHistoriesItem ||
|
||||||
|
bidHistoriesItem?.price != lotData?.currentMaxBid
|
||||||
|
) {
|
||||||
|
pushPrice({
|
||||||
|
bid_id: this.id,
|
||||||
|
price: lotData?.currentMaxBid,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
|
if (
|
||||||
if (this.current_price > this.max_price + this.plus_price) {
|
lotData.myBid &&
|
||||||
console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
|
this.max_price &&
|
||||||
return; // Dừng hàm nếu giá đã vượt qua giới hạn
|
this.max_price != lotData.myBid
|
||||||
}
|
) {
|
||||||
|
this.handlePlaceBid();
|
||||||
// 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'}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Đợi phản hồi từ API
|
|
||||||
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
|
|
||||||
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'
|
|
||||||
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');
|
|
||||||
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})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
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);
|
|
||||||
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
|
|
||||||
if (!bidValue || bidValue !== String(this.max_price)) {
|
|
||||||
console.log(`❌ Incorrect bid amount! Received: ${bidValue}`);
|
|
||||||
return; // Dừng thực hiện nếu giá trị nhập sai
|
|
||||||
}
|
|
||||||
|
|
||||||
// 📌 Nhấn nút "Place Bid"
|
|
||||||
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' });
|
|
||||||
|
|
||||||
console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
|
|
||||||
|
|
||||||
const { lotData } = await this.waitForApiResponse();
|
|
||||||
console.log(`📡 [${this.id}] API Response received:`, lotData);
|
|
||||||
|
|
||||||
// 📌 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);
|
|
||||||
|
|
||||||
sendMessage(this);
|
|
||||||
|
|
||||||
console.log(`✅ [${this.id}] Bid placed successfully!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`⚠️ [${this.id}] Bid action completed, but status is still "None".`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
|
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
|
||||||
} finally {
|
|
||||||
console.log(`🔚 [${this.id}] Resetting bid flag.`);
|
|
||||||
global[`IS_PLACE_BID-${this.id}`] = false;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
|
||||||
|
this.page_context.off("response", onResponse);
|
||||||
|
|
||||||
|
console.log(`📡 [${this.id}] Attaching new response listener...`);
|
||||||
|
this.page_context.on("response", onResponse);
|
||||||
|
|
||||||
|
console.log(`✅ [${this.id}] Navigation setup complete.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [${this.id}] Error during navigation:`, error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleCreateLogsOnServer(data) {
|
action = async () => {
|
||||||
const values = data.map((item) => {
|
try {
|
||||||
return {
|
const page = this.page_context;
|
||||||
model: item.pid,
|
|
||||||
lot_id: item.lotId,
|
|
||||||
out_price: item.lotData.minimumBid || 0,
|
|
||||||
raw_data: JSON.stringify(item),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await createOutBidLog(values);
|
// 📌 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}`);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
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...`);
|
|
||||||
|
|
||||||
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
|
||||||
// page.removeAllListeners('request');
|
|
||||||
|
|
||||||
// await page.setRequestInterception(true);
|
|
||||||
|
|
||||||
// page.on('request', (request) => {
|
|
||||||
// if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
|
|
||||||
// console.log('🚀 Fake response cho request:', request.url());
|
|
||||||
|
|
||||||
// const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8');
|
|
||||||
|
|
||||||
// request.respond({
|
|
||||||
// status: 200,
|
|
||||||
// contentType: 'application/json',
|
|
||||||
// body: fakeData,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// try {
|
|
||||||
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
const onResponse = async (response) => {
|
|
||||||
const url = response?.request()?.url();
|
|
||||||
if (!url || !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { lotData, ...prev } = await response.json();
|
|
||||||
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}] 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...`);
|
|
||||||
|
|
||||||
this.handleCreateLogsOnServer([{ lotData, ...prev }]);
|
|
||||||
} else if (['Winning'].includes(lotData?.bidStatus)) {
|
|
||||||
const bidHistoriesItem = _.maxBy(this.histories, 'price');
|
|
||||||
|
|
||||||
if (!bidHistoriesItem || bidHistoriesItem?.price != lotData?.currentMaxBid) {
|
|
||||||
pushPrice({
|
|
||||||
bid_id: this.id,
|
|
||||||
price: lotData?.currentMaxBid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
|
|
||||||
this.handlePlaceBid();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
|
|
||||||
this.page_context.off('response', onResponse);
|
|
||||||
|
|
||||||
console.log(`📡 [${this.id}] Attaching new response listener...`);
|
|
||||||
this.page_context.on('response', onResponse);
|
|
||||||
|
|
||||||
console.log(`✅ [${this.id}] Navigation setup complete.`);
|
|
||||||
} 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,367 +1,438 @@
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
import { pushPrice, updateBid } from '../../system/apis/bid.js';
|
import { pushPrice, updateBid } from "../../system/apis/bid.js";
|
||||||
import { sendMessage } from '../../system/apis/notification.js';
|
import { sendMessage } from "../../system/apis/notification.js";
|
||||||
import configs from '../../system/config.js';
|
import configs from "../../system/config.js";
|
||||||
import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
|
import {
|
||||||
import { ProductBid } from '../product-bid.js';
|
delay,
|
||||||
|
extractPriceNumber,
|
||||||
|
isTimeReached,
|
||||||
|
removeFalsyValues,
|
||||||
|
} from "../../system/utils.js";
|
||||||
|
import { ProductBid } from "../product-bid.js";
|
||||||
|
|
||||||
export class LawsonsProductBid extends ProductBid {
|
export class LawsonsProductBid extends ProductBid {
|
||||||
constructor({ ...prev }) {
|
constructor({ ...prev }) {
|
||||||
super(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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
async getReversePrice() {
|
||||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
try {
|
||||||
|
if (!this.page_context) return null;
|
||||||
|
|
||||||
if (response) {
|
await this.page_context.waitForSelector(
|
||||||
this.lot_id = response.lot_id;
|
".select-dropdown-value.text-truncate",
|
||||||
this.close_time = response.close_time;
|
{ timeout: 4000 }
|
||||||
this.start_bid_time = response.start_bid_time;
|
);
|
||||||
}
|
const price = await this.page_context.evaluate(() => {
|
||||||
|
const el = document.querySelector(
|
||||||
|
".select-dropdown-value.text-truncate"
|
||||||
|
);
|
||||||
|
return el ? el.innerText : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return price ? extractPriceNumber(price) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error.message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getReversePrice() {
|
update = async () => {
|
||||||
try {
|
try {
|
||||||
if (!this.page_context) return null;
|
if (!this.page_context) return;
|
||||||
|
|
||||||
await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
|
// if (this.updated_at) {
|
||||||
const price = await this.page_context.evaluate(() => {
|
// await this.page_context.reload({ waitUntil: 'networkidle0' });
|
||||||
const el = document.querySelector('.select-dropdown-value.text-truncate');
|
// }
|
||||||
return el ? el.innerText : null;
|
|
||||||
|
const result = await this.waitApiInfo();
|
||||||
|
|
||||||
|
const reservePrice = await this.getReversePrice();
|
||||||
|
|
||||||
|
console.log({ reservePrice });
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
// 📌 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(result?.itemView.lotId) || null,
|
||||||
|
reserve_price: reservePrice,
|
||||||
|
current_price: result?.currentBidAmount || null,
|
||||||
|
close_time: new Date(result.endTime).toUTCString() || null,
|
||||||
|
// close_time: this.close_time ? null : new Date(Date.now() + 5 * 60 * 1000).toUTCString(), //test
|
||||||
|
name: result?.itemView?.title || 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);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error Update", error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hàm con để fetch trong context trình duyệt
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
submitBid() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!this.page_context || !this.model) {
|
||||||
|
console.log(`[${this.id}] Page context or model is missing.`);
|
||||||
|
reject(null);
|
||||||
|
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({
|
||||||
|
bidAmount,
|
||||||
|
lotRef,
|
||||||
|
v2: true,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return price ? extractPriceNumber(price) : null;
|
if (!response.ok) {
|
||||||
} catch (error) {
|
throw new Error(`HTTP ${response.status}`);
|
||||||
console.log(error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update = async () => {
|
|
||||||
try {
|
|
||||||
if (!this.page_context) return;
|
|
||||||
|
|
||||||
// if (this.updated_at) {
|
|
||||||
// await this.page_context.reload({ waitUntil: 'networkidle0' });
|
|
||||||
// }
|
|
||||||
|
|
||||||
const result = await this.waitApiInfo();
|
|
||||||
|
|
||||||
const reservePrice = await this.getReversePrice();
|
|
||||||
|
|
||||||
console.log({ reservePrice });
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
// 📌 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(result?.itemView.lotId) || null,
|
|
||||||
reserve_price: reservePrice,
|
|
||||||
current_price: result?.currentBidAmount || null,
|
|
||||||
close_time: new Date(result.endTime).toUTCString() || null,
|
|
||||||
// close_time: this.close_time ? null : new Date(Date.now() + 5 * 60 * 1000).toUTCString(), //test
|
|
||||||
name: result?.itemView?.title || 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);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error Update', error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hàm con để fetch trong context trình duyệt
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
submitBid() {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
if (!this.page_context || !this.model) {
|
|
||||||
console.log(`[${this.id}] Page context or model is missing.`);
|
|
||||||
reject(null);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return await response.json();
|
||||||
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
|
},
|
||||||
|
this.max_price,
|
||||||
|
this.model,
|
||||||
|
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT
|
||||||
|
);
|
||||||
|
|
||||||
const result = await this.page_context.evaluate(
|
console.log("🧾 API Bid Result:", {
|
||||||
async (bidAmount, lotRef, url) => {
|
bid_amount: this.max_price,
|
||||||
const response = await fetch(url, {
|
result,
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
bidAmount,
|
|
||||||
lotRef,
|
|
||||||
v2: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
this.max_price,
|
|
||||||
this.model,
|
|
||||||
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('🧾 API Bid Result:', {
|
|
||||||
bid_amount: this.max_price,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result?.data?.orderBidResponse?.success) reject(null);
|
|
||||||
|
|
||||||
resolve(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
|
|
||||||
reject(null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result?.data?.orderBidResponse?.success) reject(null);
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
|
||||||
|
reject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePlaceBid() {
|
try {
|
||||||
// Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
|
console.log(`🔄 [${this.id}] Starting bid process...`);
|
||||||
if (!this.page_context) {
|
// Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại
|
||||||
console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
|
global[`IS_PLACE_BID-${this.id}`] = true;
|
||||||
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
|
// Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa
|
||||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
if (this.current_price > this.max_price + this.plus_price) {
|
||||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
console.log(`⚠️ [${this.id}] Outbid bid`);
|
||||||
return;
|
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.waitApiInfo();
|
||||||
|
|
||||||
|
// Lấy giá reserve price để kiểm tra
|
||||||
|
const reservePrice = await this.getReversePrice();
|
||||||
|
|
||||||
|
// Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
|
||||||
|
const shouldStop =
|
||||||
|
!response ||
|
||||||
|
response?.currentBidAmount > this.max_price + this.plus_price ||
|
||||||
|
response.isOutBid != true ||
|
||||||
|
!reservePrice ||
|
||||||
|
reservePrice > this.max_price + this.plus_price;
|
||||||
|
|
||||||
|
if (shouldStop) {
|
||||||
|
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");
|
||||||
|
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}] ================`
|
||||||
|
);
|
||||||
|
|
||||||
|
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) return;
|
||||||
|
|
||||||
|
console.log({ result });
|
||||||
|
|
||||||
|
// Gửi thông báo đã đấu giá thành công
|
||||||
|
sendMessage(this);
|
||||||
|
|
||||||
|
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitApiInfo() {
|
||||||
|
if (!this.page_context) {
|
||||||
|
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(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),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { ...info, ...detailData };
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackingOutbid() {
|
||||||
|
if (!this.page_context) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const onResponse = async (response) => {
|
||||||
|
const url = response?.request()?.url();
|
||||||
|
if (
|
||||||
|
!url ||
|
||||||
|
!url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`🔄 [${this.id}] Starting bid process...`);
|
const result = await response.json();
|
||||||
// Đá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 (!result) return;
|
||||||
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á
|
console.log(`📈 [${this.id}] Bid data: `, result);
|
||||||
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 { maxBidAmount, currentBidAmount, isOutBid } = result;
|
||||||
const response = await this.waitApiInfo();
|
|
||||||
|
|
||||||
// Lấy giá reserve price để kiểm tra
|
console.log(
|
||||||
const reservePrice = await this.getReversePrice();
|
`📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`
|
||||||
|
);
|
||||||
|
|
||||||
// Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
|
// Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
|
||||||
const shouldStop =
|
const reversePrice = await this.getReversePrice();
|
||||||
!response ||
|
console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
|
||||||
response?.currentBidAmount > this.max_price + this.plus_price ||
|
|
||||||
response.isOutBid != true ||
|
|
||||||
!reservePrice ||
|
|
||||||
reservePrice > this.max_price + this.plus_price;
|
|
||||||
|
|
||||||
if (shouldStop) {
|
// Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
|
||||||
console.log(`⚠️ [${this.id}] Stop bidding:`, { reservePrice, currentBidAmount: response?.currentBidAmount, maxBidAmount: response?.maxBidAmount });
|
const bidHistoriesItem = _.maxBy(this.histories, "price");
|
||||||
return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
|
console.log(
|
||||||
}
|
`📈 [${this.id}] Highest local bid: ${
|
||||||
|
bidHistoriesItem?.price ?? "N/A"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
// Tìm bid history lớn nhất từ các lịch sử đấu giá của item
|
if (!this.close_time || !this.lot_id || !this.current_price) return;
|
||||||
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
|
// Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
|
||||||
if (bidHistoriesItem && bidHistoriesItem?.price == this.current_price && this.max_price + this.plus_price == response?.maxBidAmount) {
|
if (
|
||||||
console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`);
|
(!bidHistoriesItem && maxBidAmount) ||
|
||||||
return;
|
(bidHistoriesItem?.price != currentBidAmount &&
|
||||||
}
|
currentBidAmount == maxBidAmount)
|
||||||
|
) {
|
||||||
if (this.reserve_price <= 0) {
|
console.log(
|
||||||
console.log(`[${this.reserve_price}]`);
|
`🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`
|
||||||
return;
|
);
|
||||||
}
|
pushPrice({
|
||||||
|
bid_id: this.id,
|
||||||
console.log(`===============Start call to submit [${this.id}] ================`);
|
price: currentBidAmount,
|
||||||
|
});
|
||||||
await delay(20000);
|
}
|
||||||
|
|
||||||
// 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) return;
|
|
||||||
|
|
||||||
console.log({ result });
|
|
||||||
|
|
||||||
// Gửi thông báo đã đấu giá thành công
|
|
||||||
sendMessage(this);
|
|
||||||
|
|
||||||
await this.page_context.reload({ waitUntil: 'networkidle0' });
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitApiInfo() {
|
|
||||||
if (!this.page_context) {
|
|
||||||
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(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)]);
|
|
||||||
|
|
||||||
return { ...info, ...detailData };
|
|
||||||
}
|
|
||||||
|
|
||||||
async trackingOutbid() {
|
|
||||||
if (!this.page_context) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const onResponse = async (response) => {
|
|
||||||
const url = response?.request()?.url();
|
|
||||||
if (!url || !url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
console.log(`📈 [${this.id}] Bid data: `, result);
|
|
||||||
|
|
||||||
const { maxBidAmount, currentBidAmount, isOutBid } = result;
|
|
||||||
|
|
||||||
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'}`);
|
|
||||||
|
|
||||||
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: 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 && 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' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
|
|
||||||
this.page_context.off('response', onResponse);
|
|
||||||
|
|
||||||
console.log(`📡 [${this.id}] Attaching new response listener...`);
|
|
||||||
this.page_context.on('response', onResponse);
|
|
||||||
|
|
||||||
console.log(`✅ [${this.id}] Navigation setup complete.`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ [${this.id}] Error during navigation:`, 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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...`
|
||||||
|
);
|
||||||
await this.handlePlaceBid();
|
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" });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
|
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
|
||||||
|
this.page_context.off("response", onResponse);
|
||||||
|
|
||||||
|
console.log(`📡 [${this.id}] Attaching new response listener...`);
|
||||||
|
this.page_context.on("response", onResponse);
|
||||||
|
|
||||||
|
console.log(`✅ [${this.id}] Navigation setup complete.`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [${this.id}] Error during navigation:`, 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,159 +1,161 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import path from 'path';
|
import BID_TYPE from "../system/bid-type.js";
|
||||||
import BID_TYPE from '../system/bid-type.js';
|
import browser from "../system/browser.js";
|
||||||
import browser from '../system/browser.js';
|
import { getPathProfile } from "../system/utils.js";
|
||||||
import CONSTANTS from '../system/constants.js';
|
import { Bid } from "./bid.js";
|
||||||
import { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
|
|
||||||
import { Bid } from './bid.js';
|
|
||||||
|
|
||||||
export class ProductBid extends Bid {
|
export class ProductBid extends Bid {
|
||||||
id;
|
id;
|
||||||
max_price;
|
max_price;
|
||||||
model;
|
model;
|
||||||
lot_id;
|
lot_id;
|
||||||
plus_price;
|
plus_price;
|
||||||
close_time;
|
close_time;
|
||||||
first_bid;
|
first_bid;
|
||||||
quantity;
|
quantity;
|
||||||
created_at;
|
created_at;
|
||||||
updated_at;
|
updated_at;
|
||||||
histories;
|
histories;
|
||||||
start_bid_time;
|
start_bid_time;
|
||||||
parent_browser_context;
|
parent_browser_context;
|
||||||
web_bid;
|
web_bid;
|
||||||
current_price;
|
current_price;
|
||||||
name;
|
name;
|
||||||
reserve_price;
|
reserve_price;
|
||||||
update;
|
update;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
max_price,
|
max_price,
|
||||||
plus_price,
|
plus_price,
|
||||||
model,
|
model,
|
||||||
first_bid = false,
|
first_bid = false,
|
||||||
id,
|
id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
quantity = 1,
|
quantity = 1,
|
||||||
histories = [],
|
histories = [],
|
||||||
close_time,
|
close_time,
|
||||||
lot_id,
|
lot_id,
|
||||||
start_bid_time,
|
start_bid_time,
|
||||||
web_bid,
|
web_bid,
|
||||||
current_price,
|
current_price,
|
||||||
reserve_price,
|
reserve_price,
|
||||||
name,
|
name,
|
||||||
}) {
|
}) {
|
||||||
super(BID_TYPE.PRODUCT_TAB, url);
|
super(BID_TYPE.PRODUCT_TAB, url);
|
||||||
this.max_price = max_price || 0;
|
this.max_price = max_price || 0;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.plus_price = plus_price || 0;
|
this.plus_price = plus_price || 0;
|
||||||
this.first_bid = first_bid;
|
this.first_bid = first_bid;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.created_at = created_at;
|
this.created_at = created_at;
|
||||||
this.updated_at = updated_at;
|
this.updated_at = updated_at;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.histories = histories;
|
this.histories = histories;
|
||||||
this.close_time = close_time;
|
this.close_time = close_time;
|
||||||
this.lot_id = lot_id;
|
this.lot_id = lot_id;
|
||||||
this.start_bid_time = start_bid_time;
|
this.start_bid_time = start_bid_time;
|
||||||
this.web_bid = web_bid;
|
this.web_bid = web_bid;
|
||||||
this.current_price = current_price;
|
this.current_price = current_price;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.reserve_price = reserve_price;
|
this.reserve_price = reserve_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewData({
|
||||||
|
url,
|
||||||
|
max_price,
|
||||||
|
plus_price,
|
||||||
|
model,
|
||||||
|
first_bid = false,
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
quantity = 1,
|
||||||
|
histories = [],
|
||||||
|
close_time,
|
||||||
|
lot_id,
|
||||||
|
start_bid_time,
|
||||||
|
web_bid,
|
||||||
|
current_price,
|
||||||
|
reserve_price,
|
||||||
|
name,
|
||||||
|
}) {
|
||||||
|
this.max_price = max_price || 0;
|
||||||
|
this.model = model;
|
||||||
|
this.plus_price = plus_price || 0;
|
||||||
|
this.first_bid = first_bid;
|
||||||
|
this.id = id;
|
||||||
|
this.created_at = created_at;
|
||||||
|
this.updated_at = updated_at;
|
||||||
|
this.quantity = quantity;
|
||||||
|
this.histories = histories;
|
||||||
|
this.close_time = close_time;
|
||||||
|
this.lot_id = lot_id;
|
||||||
|
this.start_bid_time = start_bid_time;
|
||||||
|
this.web_bid = web_bid;
|
||||||
|
this.url = url;
|
||||||
|
this.current_price = current_price;
|
||||||
|
this.name = name;
|
||||||
|
this.reserve_price = reserve_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
puppeteer_connect = async () => {
|
||||||
|
if (!this.parent_browser_context) {
|
||||||
|
console.log(
|
||||||
|
`❌ Connect fail. parent_browser_context is null: ${this.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewData({
|
const context = await browser.createBrowserContext();
|
||||||
url,
|
|
||||||
max_price,
|
const statusInit = await this.restoreContext(context);
|
||||||
plus_price,
|
|
||||||
model,
|
if (!statusInit) {
|
||||||
first_bid = false,
|
console.log(`⚠️ Restore failed.`);
|
||||||
id,
|
return;
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
quantity = 1,
|
|
||||||
histories = [],
|
|
||||||
close_time,
|
|
||||||
lot_id,
|
|
||||||
start_bid_time,
|
|
||||||
web_bid,
|
|
||||||
current_price,
|
|
||||||
reserve_price,
|
|
||||||
name,
|
|
||||||
}) {
|
|
||||||
this.max_price = max_price || 0;
|
|
||||||
this.model = model;
|
|
||||||
this.plus_price = plus_price || 0;
|
|
||||||
this.first_bid = first_bid;
|
|
||||||
this.id = id;
|
|
||||||
this.created_at = created_at;
|
|
||||||
this.updated_at = updated_at;
|
|
||||||
this.quantity = quantity;
|
|
||||||
this.histories = histories;
|
|
||||||
this.close_time = close_time;
|
|
||||||
this.lot_id = lot_id;
|
|
||||||
this.start_bid_time = start_bid_time;
|
|
||||||
this.web_bid = web_bid;
|
|
||||||
this.url = url;
|
|
||||||
this.current_price = current_price;
|
|
||||||
this.name = name;
|
|
||||||
this.reserve_price = reserve_price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
puppeteer_connect = async () => {
|
const page = await context.newPage();
|
||||||
if (!this.parent_browser_context) {
|
|
||||||
console.log(`❌ Connect fail. parent_browser_context is null: ${this.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = await browser.createBrowserContext();
|
this.page_context = page;
|
||||||
|
};
|
||||||
|
|
||||||
const statusInit = await this.restoreContext(context);
|
async restoreContext(context) {
|
||||||
|
const filePath = getPathProfile(this.web_bid.origin_url);
|
||||||
|
|
||||||
if (!statusInit) {
|
if (!fs.existsSync(filePath)) return false;
|
||||||
console.log(`⚠️ Restore failed.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = await context.newPage();
|
const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
|
|
||||||
this.page_context = page;
|
// Restore Cookies
|
||||||
};
|
await context.setCookie(...contextData.cookies);
|
||||||
|
|
||||||
async restoreContext(context) {
|
return true;
|
||||||
const filePath = getPathProfile(this.web_bid.origin_url);
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) return false;
|
async gotoLink() {
|
||||||
|
const page = this.page_context;
|
||||||
|
|
||||||
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
if (page.isClosed()) {
|
||||||
|
console.error("❌ Page has been closed, cannot navigate.");
|
||||||
// Restore Cookies
|
return;
|
||||||
await context.setCookie(...contextData.cookies);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async gotoLink() {
|
console.log("🔄 Starting the bidding process...");
|
||||||
const page = this.page_context;
|
|
||||||
|
|
||||||
if (page.isClosed()) {
|
try {
|
||||||
console.error('❌ Page has been closed, cannot navigate.');
|
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||||
return;
|
console.log(`✅ Navigated to: ${this.url}`);
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔄 Starting the bidding process...');
|
await page.bringToFront();
|
||||||
|
await page.setUserAgent(
|
||||||
try {
|
"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.goto(this.url, { waitUntil: 'networkidle2' });
|
);
|
||||||
console.log(`✅ Navigated to: ${this.url}`);
|
console.log("👀 Brought the tab to the foreground.");
|
||||||
|
} catch (error) {
|
||||||
await page.bringToFront();
|
console.error("❌ Error during navigation:", error);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,82 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
|
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
|
||||||
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
|
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
|
||||||
import { LangtonsApiBid } from '../models/langtons.com.au/langtons-api-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 { LangtonsProductBid } from "../models/langtons.com.au/langtons-product-bid.js";
|
||||||
import configs from '../system/config.js';
|
import { LawsonsApiBid } from "../models/lawsons.com.au/lawsons-api-bid.js";
|
||||||
import CONSTANTS from '../system/constants.js';
|
import { LawsonsProductBid } from "../models/lawsons.com.au/lawsons-product-bid.js";
|
||||||
import { sanitizeFileName } from '../system/utils.js';
|
import { PicklesApiBid } from "../models/pickles.com.au/pickles-api-bid.js";
|
||||||
import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js';
|
import { PicklesProductBid } from "../models/pickles.com.au/pickles-product-bid.js";
|
||||||
import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-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;
|
const TIME = 30 * 1000;
|
||||||
|
|
||||||
export const handleCloseRemoveProduct = (data) => {
|
export const handleCloseRemoveProduct = (data) => {
|
||||||
if (!Array.isArray(data)) return;
|
if (!Array.isArray(data)) return;
|
||||||
|
|
||||||
data.forEach(async (item) => {
|
data.forEach(async (item) => {
|
||||||
if (item.page_context) {
|
if (item.page_context) {
|
||||||
safeClosePage(item);
|
safeClosePage(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createBidProduct = (web, data) => {
|
export const createBidProduct = (web, data) => {
|
||||||
switch (web.origin_url) {
|
switch (web.origin_url) {
|
||||||
case configs.WEB_URLS.GRAYS: {
|
case configs.WEB_URLS.GRAYS: {
|
||||||
return new GraysProductBid({ ...data });
|
return new GraysProductBid({ ...data });
|
||||||
}
|
|
||||||
case configs.WEB_URLS.LANGTONS: {
|
|
||||||
return new LangtonsProductBid({ ...data });
|
|
||||||
}
|
|
||||||
case configs.WEB_URLS.LAWSONS: {
|
|
||||||
return new LawsonsProductBid({ ...data });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
case configs.WEB_URLS.LANGTONS: {
|
||||||
|
return new LangtonsProductBid({ ...data });
|
||||||
|
}
|
||||||
|
case configs.WEB_URLS.LAWSONS: {
|
||||||
|
return new LawsonsProductBid({ ...data });
|
||||||
|
}
|
||||||
|
case configs.WEB_URLS.PICKLES: {
|
||||||
|
return new PicklesProductBid({ ...data });
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createApiBid = (web) => {
|
export const createApiBid = (web) => {
|
||||||
switch (web.origin_url) {
|
switch (web.origin_url) {
|
||||||
case configs.WEB_URLS.GRAYS: {
|
case configs.WEB_URLS.GRAYS: {
|
||||||
return new GrayApiBid({ ...web });
|
return new GrayApiBid({ ...web });
|
||||||
}
|
|
||||||
case configs.WEB_URLS.LANGTONS: {
|
|
||||||
return new LangtonsApiBid({ ...web });
|
|
||||||
}
|
|
||||||
case configs.WEB_URLS.LAWSONS: {
|
|
||||||
return new LawsonsApiBid({ ...web });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
case configs.WEB_URLS.LANGTONS: {
|
||||||
|
return new LangtonsApiBid({ ...web });
|
||||||
|
}
|
||||||
|
case configs.WEB_URLS.LAWSONS: {
|
||||||
|
return new LawsonsApiBid({ ...web });
|
||||||
|
}
|
||||||
|
case configs.WEB_URLS.PICKLES: {
|
||||||
|
return new PicklesApiBid({ ...web });
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteProfile = (data) => {
|
export const deleteProfile = (data) => {
|
||||||
if (!data?.origin_url) return false;
|
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)) {
|
if (fs.existsSync(filePath)) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldUpdateProductTab = (productTab) => {
|
export const shouldUpdateProductTab = (productTab) => {
|
||||||
const updatedAt = new Date(productTab.updated_at).getTime();
|
const updatedAt = new Date(productTab.updated_at).getTime();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
return now - updatedAt >= TIME;
|
return now - updatedAt >= TIME;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,125 +1,143 @@
|
||||||
import axios from '../axios.js';
|
import axios from "../axios.js";
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
export const getBids = async () => {
|
export const getBids = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: 'bids',
|
url: "bids",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ DATA IS NOT FOUND ON SERVER');
|
console.log("❌ DATA IS NOT FOUND ON SERVER");
|
||||||
return [];
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER (GET BIDS): ', error);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER (GET BIDS): ", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBid = async (id, values) => {
|
export const updateBid = async (id, values) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
url: 'bids/' + id,
|
url: "bids/" + id,
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ UPDATE FAILURE (UPDATE BID)');
|
console.log("❌ UPDATE FAILURE (UPDATE BID)");
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER: (UPDATE BID) ", error.response);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const outBid = async (id) => {
|
export const outBid = async (id) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: 'bids/out-bid/' + id,
|
url: "bids/out-bid/" + id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ OUT BID UPDATE FAILURE');
|
console.log("❌ OUT BID UPDATE FAILURE");
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER (OUT BID UPDATE): ', error);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER (OUT BID UPDATE): ", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pushPrice = async (values) => {
|
export const pushPrice = async (values) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: 'bid-histories',
|
url: "bid-histories",
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ PUSH PRICE FAILURE');
|
console.log("❌ PUSH PRICE FAILURE");
|
||||||
return { status: false, data: [] };
|
return { status: false, data: [] };
|
||||||
}
|
|
||||||
|
|
||||||
return { status: true, data: data.data };
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error?.response);
|
|
||||||
return { status: false, data: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { status: true, data: data.data };
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER (PUSH PRICE): ", error?.response);
|
||||||
|
return { status: false, data: [] };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStatusByPrice = async (id, current_price) => {
|
export const updateStatusByPrice = async (id, current_price) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: 'bids/update-status/' + id,
|
url: "bids/update-status/" + id,
|
||||||
data: {
|
data: {
|
||||||
current_price: Number(current_price) | 0,
|
current_price: Number(current_price) | 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ UPDATE STATUS BY PRICE FAILURE');
|
console.log("❌ UPDATE STATUS BY PRICE FAILURE");
|
||||||
return { status: false, data: [] };
|
return { status: false, data: [] };
|
||||||
}
|
|
||||||
|
|
||||||
return { status: true, data: data.data };
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', {
|
|
||||||
// response: error.response,
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
return { status: false, data: [] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { status: true, data: data.data };
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ", {
|
||||||
|
// response: error.response,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return { status: false, data: [] };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStatusWork = async (item, filePath) => {
|
export const updateStatusWork = async (item, filePath) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
url: `bids/update-status-work/${item.type}/${item.id}`,
|
url: `bids/update-status-work/${item.type}/${item.id}`,
|
||||||
data: {
|
data: {
|
||||||
image: fs.createReadStream(filePath),
|
image: fs.createReadStream(filePath),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
|
||||||
return response.data?.data;
|
return response.data?.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Upload failed:', error.response?.data || error.message);
|
console.error("❌ Upload failed:", error.response?.data || error.message);
|
||||||
return false;
|
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) => {
|
export const sendMessage = async (values) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: 'notifications/send-messages',
|
url: "notifications/send-messages",
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ UPDATE FAILURE (UPDATE Noti)');
|
console.log("❌ UPDATE FAILURE (UPDATE Noti)");
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return data.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('❌ ERROR IN SERVER: (UPDATE Noti) ', error);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ ERROR IN SERVER: (UPDATE Noti) ", error.response);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,45 @@
|
||||||
const configs = {
|
const configs = {
|
||||||
AUTO_TRACKING_DELAY: 5000,
|
AUTO_TRACKING_DELAY: 5000,
|
||||||
AUTO_TRACKING_CLEANING: 10000,
|
AUTO_TRACKING_CLEANING: 10000,
|
||||||
SOCKET_URL: process.env.SOCKET_URL,
|
SOCKET_URL: process.env.SOCKET_URL,
|
||||||
WEB_URLS: {
|
WEB_URLS: {
|
||||||
GRAYS: `https://www.grays.com`,
|
GRAYS: `https://www.grays.com`,
|
||||||
LANGTONS: `https://www.langtons.com.au`,
|
LANGTONS: `https://www.langtons.com.au`,
|
||||||
LAWSONS: `https://www.lawsons.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",
|
||||||
},
|
},
|
||||||
WEB_CONFIGS: {
|
LANGTONS: {
|
||||||
GRAYS: {
|
AUTO_CALL_API_TO_TRACKING: 5000,
|
||||||
AUTO_CALL_API_TO_TRACKING: 3000,
|
LOGIN_URL: "https://www.langtons.com.au/account/login",
|
||||||
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
|
API_CALL_TO_TRACKING:
|
||||||
},
|
"https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData",
|
||||||
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',
|
|
||||||
},
|
|
||||||
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',
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
LAWSONS: {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default configs;
|
export default configs;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue