pickcels
This commit is contained in:
parent
173841c57c
commit
b13712a317
|
|
@ -38,6 +38,7 @@
|
|||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"uuid": "^11.0.5",
|
||||
"yet-another-react-lightbox": "^3.22.0",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
|
@ -1422,7 +1423,7 @@
|
|||
"version": "19.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
|
||||
"integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
|
|
@ -7541,6 +7542,29 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yet-another-react-lightbox": {
|
||||
"version": "3.22.0",
|
||||
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.22.0.tgz",
|
||||
"integrity": "sha512-yaXmzUraH/Ftsp7eG/E2leQgXhtrG8c1t+jImlSjC2XtZ7XkvjIV2vP/1kl5kxmsBHjck/98W/9Xxempry+2QQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16 || ^17 || ^18 || ^19",
|
||||
"@types/react-dom": "^16 || ^17 || ^18 || ^19",
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19",
|
||||
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"uuid": "^11.0.5",
|
||||
"yet-another-react-lightbox": "^3.22.0",
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { generateNestParams, handleError, handleSuccess } from '.';
|
||||
import axios from '../lib/axios';
|
||||
import { IAdmin } from '../system/type';
|
||||
|
|
@ -51,7 +52,8 @@ export const grantNewPasswordAdmin = async (admin: Partial<IAdmin>) => {
|
|||
};
|
||||
|
||||
export const createAdmin = async (admin: Omit<IAdmin, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
|
||||
const newData = removeFalsyValues(admin);
|
||||
const {permissions , ...newData} = removeFalsyValues(admin);
|
||||
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
|
|
|
|||
|
|
@ -34,3 +34,18 @@ export const shutdownTool = async () => {
|
|||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const getStatusTool = async () => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/status-tool`,
|
||||
withCredentials: true,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
|
@ -9,7 +9,7 @@ export const handleError = (error: unknown) => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response = (error as AxiosError).response as Record<string, any>;
|
||||
|
||||
const data = response.data;
|
||||
const data = response?.data;
|
||||
|
||||
if (response.status === HttpStatusCode.Forbidden) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -127,14 +127,14 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
|
|||
centered
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
<TextInput readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
|
||||
<TextInput size="sm" label="Email" {...form.getInputProps('email')} />
|
||||
<TextInput className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
|
||||
<TextInput withAsterisk readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
|
||||
<TextInput withAsterisk size="sm" label="Email" {...form.getInputProps('email')} />
|
||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
|
||||
|
||||
{!data && (
|
||||
<>
|
||||
<PasswordInput size="sm" label="Password" {...form.getInputProps('password')} />
|
||||
<PasswordInput size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||
<PasswordInput withAsterisk size="sm" label="Password" {...form.getInputProps('password')} />
|
||||
<PasswordInput withAsterisk size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
|
|||
centered
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
|
||||
<PasswordInput className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
|
||||
<PasswordInput className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||
<PasswordInput withAsterisk className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
|
||||
<PasswordInput withAsterisk className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
|
||||
|
||||
<Button className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
{'Grant'}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { default as ShowHistoriesModal } from './show-histories-modal';
|
||||
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
|
||||
export { default as ShowHistoriesBidPicklesApiModal } from './show-histories-bid-pickles-api-modal';
|
||||
export { default as BidModal } from './bid-modal';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
|
|||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return histories.map((element) => (
|
||||
<Table.Tr key={element.LotId}>
|
||||
return histories.map((element, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
|
||||
<Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||
<Table.Td>{`AU $${element['Price']}`}</Table.Td>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { getDetailBidHistories } from '../../apis/bid-histories';
|
||||
import { IBid } from '../../system/type';
|
||||
import { formatTime } from '../../utils';
|
||||
|
||||
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
|
||||
data: IBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
|
||||
const [histories, setHistories] = useState<Record<string, string>[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return histories.map((element, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{element['bidderAnonName']}</Table.Td>
|
||||
<Table.Td>{element['actualBid']}</Table.Td>
|
||||
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())}</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 */
|
||||
import { Image, Modal, ModalProps, ScrollArea } from '@mantine/core';
|
||||
|
||||
export default function ShowImageModal({ src, fallbackSrc, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
|
||||
import { ModalProps } from '@mantine/core';
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import "yet-another-react-lightbox/plugins/captions.css";
|
||||
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
|
||||
import "yet-another-react-lightbox/plugins/thumbnails.css";
|
||||
import Zoom from "yet-another-react-lightbox/plugins/zoom";
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
export default function ShowImageModal({ src, fallbackSrc,opened, onClose, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
|
||||
return (
|
||||
<Modal
|
||||
classNames={{
|
||||
header: '!flex !item-center !justify-center w-full',
|
||||
}}
|
||||
{...props}
|
||||
size={'xl'}
|
||||
title={<span className="text-xl font-bold">Image</span>}
|
||||
centered
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
>
|
||||
<Image src={src} fallbackSrc={fallbackSrc} />
|
||||
</Modal>
|
||||
<Lightbox {...props} open={opened} close={onClose} slides={[{ src: src || fallbackSrc }]}
|
||||
plugins={[Fullscreen, Zoom]}/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,104 +1,196 @@
|
|||
import { Box, Button, Image, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { getImagesWorking } from '../../apis/bid';
|
||||
import { IBid, IWebBid } from '../../system/type';
|
||||
import ShowImageModal from './show-image-modal';
|
||||
|
||||
import { Badge, Box, Button, Image, Text } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import moment from "moment";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { getImagesWorking } from "../../apis/bid";
|
||||
import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store";
|
||||
import { IBid, IWebBid } from "../../system/type";
|
||||
import { cn, stringToColor } from "../../utils";
|
||||
import ShowImageModal from "./show-image-modal";
|
||||
export interface IWorkingPageProps {
|
||||
data: (IBid | IWebBid) & { type: string };
|
||||
socket: Socket;
|
||||
data: (IBid | IWebBid) & { type: string };
|
||||
socket: Socket;
|
||||
}
|
||||
|
||||
export default function WorkingPage({ data, socket }: IWorkingPageProps) {
|
||||
const fallbackSrc = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s';
|
||||
const fallbackSrc =
|
||||
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s";
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
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 {
|
||||
return 'name' in obj;
|
||||
const [payloadLoginStatus, setPayloadLoginStatus] = useState<{
|
||||
data: IWebBid;
|
||||
login_status: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const { statusTool } = useStatusToolStore();
|
||||
|
||||
function isIBid(obj: IBid | IWebBid): obj is IBid {
|
||||
return "name" in obj;
|
||||
}
|
||||
|
||||
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) => {
|
||||
return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`;
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
return Number(filename.split('-')[0]) || 0;
|
||||
socket.on("working", updateImage);
|
||||
|
||||
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 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 origin_url = isIBid(data) ? data.web_bid.origin_url : data.origin_url;
|
||||
|
||||
socket.on('working', updateImage);
|
||||
socket.on(`login-status.${origin_url}`, onLoginStatus);
|
||||
|
||||
return () => {
|
||||
socket.off('working', updateImage);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [socket, data.id, data.type]);
|
||||
return () => {
|
||||
socket.off(`login-status.${origin_url}`, onLoginStatus);
|
||||
};
|
||||
}, [data, socket]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await getImagesWorking(data);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
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));
|
||||
setLastUpdate(new Date(extractTime(filename)));
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
setImageSrc(renderUrl(data, filename));
|
||||
setLastUpdate(new Date(extractTime(filename)));
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className="rounded-md overflow-hidden relative shadow-lg">
|
||||
<Image
|
||||
radius="md"
|
||||
h={300}
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
fallbackSrc={fallbackSrc}
|
||||
src={imageSrc}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className={cn("rounded-md overflow-hidden relative shadow-lg", {
|
||||
["border border-green-800"]: payloadLoginStatus?.login_status,
|
||||
["border border-red-800"]: !payloadLoginStatus?.login_status,
|
||||
})}
|
||||
>
|
||||
<Image
|
||||
radius="md"
|
||||
h={300}
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
}}
|
||||
fallbackSrc={fallbackSrc}
|
||||
src={imageSrc}
|
||||
/>
|
||||
|
||||
<Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
|
||||
<Text className="text-lg tracking-wide text-center font-bold">{isIBid(data) ? data.name : 'Tracking page'}</Text>
|
||||
{isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
|
||||
{isIBid(data) && <Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>}
|
||||
<Text className="text-sm italic opacity-80">{moment(lastUpdate).format('HH:mm:ss DD/MM/YYYY')}</Text>
|
||||
<Box className="flex items-center gap-4">
|
||||
<Button size="xs" color="green" onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
|
||||
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>
|
||||
</Box>
|
||||
<Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
|
||||
<Text className="text-lg tracking-wide text-center font-bold">
|
||||
{isIBid(data) ? data.name : "Tracking page"}
|
||||
</Text>
|
||||
{isIBid(data) && (
|
||||
<Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>
|
||||
)}
|
||||
{isIBid(data) && (
|
||||
<Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
|
||||
)}
|
||||
<Text className="text-sm italic opacity-80">
|
||||
{moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
|
||||
</Text>
|
||||
<Box className="flex items-center gap-4">
|
||||
<Button
|
||||
size="xs"
|
||||
color="green"
|
||||
onClick={open}
|
||||
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
|
||||
>
|
||||
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
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
<TextInput className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
|
||||
<TextInput className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
|
||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
|
||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
|
||||
|
||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
{data ? 'Update' : 'Create'}
|
||||
|
|
|
|||
|
|
@ -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 { useDisclosure } from '@mantine/hooks';
|
||||
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
|
||||
import _ from 'lodash';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
||||
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
|
||||
import Table from '../lib/table/table';
|
||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||
import { mappingStatusColors } from '../system/constants';
|
||||
import { IBid } from '../system/type';
|
||||
import { formatTime } from '../utils';
|
||||
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import {
|
||||
IconAd,
|
||||
IconAdOff,
|
||||
IconEdit,
|
||||
IconHammer,
|
||||
IconHistory,
|
||||
IconMenu,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import _ from "lodash";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { deleteBid, deletesBid, getBids, toggleBid } from "../apis/bid";
|
||||
import {
|
||||
BidModal,
|
||||
ShowHistoriesBidGraysApiModal,
|
||||
ShowHistoriesBidPicklesApiModal,
|
||||
ShowHistoriesModal,
|
||||
} from "../components/bid";
|
||||
import Table from "../lib/table/table";
|
||||
import { IColumn, TRefTableFn } from "../lib/table/type";
|
||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
|
||||
import { mappingStatusColors } from "../system/constants";
|
||||
import { IBid } from "../system/type";
|
||||
import { formatTime } from "../utils";
|
||||
import constants, { haveHistories } from "../constant";
|
||||
|
||||
export default function Bids() {
|
||||
const refTableFn: TRefTableFn<IBid> = useRef({});
|
||||
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 [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
|
||||
const [openedBid, bidModal] = useDisclosure(false);
|
||||
const [openedHistories, historiesModel] = useDisclosure(false);
|
||||
const [openedHistoriesGraysApi, historiesGraysApiModel] =
|
||||
useDisclosure(false);
|
||||
|
||||
const columns: IColumn<IBid>[] = [
|
||||
{
|
||||
key: 'id',
|
||||
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 [openedHistoriesPicklesApi, historiesPicklesApiModel] =
|
||||
useDisclosure(false);
|
||||
const [openedBid, bidModal] = useDisclosure(false);
|
||||
|
||||
{
|
||||
key: 'plus_price',
|
||||
title: 'Plus price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'max_price',
|
||||
title: 'Max price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'current_price',
|
||||
title: 'Current price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'reserve_price',
|
||||
title: 'Reserve price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'histories',
|
||||
title: 'Current bid',
|
||||
typeFilter: 'none',
|
||||
renderRow(row) {
|
||||
const bidPrice = _.maxBy(row.histories, 'price');
|
||||
const columns: IColumn<IBid>[] = [
|
||||
{
|
||||
key: "id",
|
||||
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",
|
||||
},
|
||||
|
||||
return <Text>{bidPrice ? bidPrice.price : 'None'}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'start_bid_time',
|
||||
title: 'Start bid',
|
||||
typeFilter: 'text',
|
||||
renderRow(row) {
|
||||
return (
|
||||
<Tooltip hidden={!row.start_bid_time} label={row.start_bid_time}>
|
||||
<Text size="sm">{row.start_bid_time ? formatTime(row.start_bid_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
{
|
||||
key: "plus_price",
|
||||
title: "Plus price",
|
||||
typeFilter: "number",
|
||||
},
|
||||
{
|
||||
key: "max_price",
|
||||
title: "Max price",
|
||||
typeFilter: "number",
|
||||
},
|
||||
{
|
||||
key: "current_price",
|
||||
title: "Current price",
|
||||
typeFilter: "number",
|
||||
},
|
||||
{
|
||||
key: "reserve_price",
|
||||
title: "Reserve price",
|
||||
typeFilter: "number",
|
||||
},
|
||||
{
|
||||
key: "histories",
|
||||
title: "Current bid",
|
||||
typeFilter: "none",
|
||||
renderRow(row) {
|
||||
const bidPrice = _.maxBy(row.histories, "price");
|
||||
|
||||
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 <Text>{bidPrice ? bidPrice.price : "None"}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "start_bid_time",
|
||||
title: "Start bid",
|
||||
typeFilter: "text",
|
||||
renderRow(row) {
|
||||
return (
|
||||
<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);
|
||||
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
// 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 (
|
||||
<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
|
||||
opened={openedHistories}
|
||||
onClose={() => {
|
||||
historiesModel.close();
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
<BidModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
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>
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedBid}
|
||||
onClose={() => {
|
||||
bidModal.close();
|
||||
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
bidModal.open();
|
||||
}}
|
||||
leftSection={<IconEdit size={14} />}
|
||||
>
|
||||
Edit
|
||||
</Menu.Item>
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
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
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
<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>
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedHistoriesGraysApi}
|
||||
onClose={() => {
|
||||
historiesGraysApiModel.close();
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
</Box>
|
||||
<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
|
||||
}, []);
|
||||
|
||||
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 */
|
||||
import { Box, Button, LoadingOverlay, Text, Title } from '@mantine/core';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import io from 'socket.io-client';
|
||||
import { WorkingPage } from '../components/dashboard';
|
||||
import { IBid, IWebBid } from '../system/type';
|
||||
import { checkStatus } from '../apis/auth';
|
||||
import { IconPower, IconRestore } from '@tabler/icons-react';
|
||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||
import { resetTool, shutdownTool } from '../apis/dashboard';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import io from "socket.io-client";
|
||||
import { WorkingPage } from "../components/dashboard";
|
||||
import { IBid, IWebBid } from "../system/type";
|
||||
import { checkStatus } from "../apis/auth";
|
||||
import { IconPower, IconRestore } from "@tabler/icons-react";
|
||||
import { useConfirmStore } from "../lib/zustand/use-confirm";
|
||||
import { getStatusTool, resetTool, shutdownTool } from "../apis/dashboard";
|
||||
import { cn } from "../utils";
|
||||
import { useStatusToolStore } from "../lib/zustand/use-status-tool-store";
|
||||
|
||||
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
|
||||
autoConnect: true,
|
||||
transports: ['websocket'],
|
||||
autoConnect: true,
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
export default function DashBoard() {
|
||||
const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
|
||||
const { setConfirm } = useConfirmStore();
|
||||
const [workingData, setWorkingData] = useState<
|
||||
(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.on('connect', () => {
|
||||
socket.emit('getBidsData');
|
||||
});
|
||||
RETRY_CONNECT.current--;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
if (RETRY_CONNECT.current > 0) {
|
||||
await checkStatus();
|
||||
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[]);
|
||||
|
||||
socket.connect();
|
||||
const newData = array.map((item) => {
|
||||
if (item.children) {
|
||||
return {
|
||||
...item,
|
||||
type: "API_BID",
|
||||
};
|
||||
}
|
||||
|
||||
RETRY_CONNECT.current--;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
return {
|
||||
...item,
|
||||
type: "PRODUCT_TAB",
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
setWorkingData(newData);
|
||||
});
|
||||
|
||||
const handleResetTool = () => {
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
await resetTool();
|
||||
setLoading(false);
|
||||
},
|
||||
title: 'Confirm tool reset',
|
||||
message: 'Are you sure you want to reset this tool? All current processes will be stopped and restarted.',
|
||||
okButton: { value: 'Ok', color: 'blue' },
|
||||
});
|
||||
return () => {
|
||||
console.log("🔌 Cleanup WebSocket listeners...");
|
||||
socket.off("adminBidsUpdated");
|
||||
socket.off("working");
|
||||
socket.off("connect");
|
||||
socket.off("disconnect");
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const statusTool = async () => {
|
||||
const result = await getStatusTool();
|
||||
|
||||
if (result?.data) {
|
||||
setStatusTool(result?.data);
|
||||
} else {
|
||||
setStatusTool(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShutdownTool = () => {
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
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' },
|
||||
});
|
||||
const intervalId = setInterval(statusTool, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box className="flex items-center justify-between">
|
||||
<Title order={2} mb="md">
|
||||
Admin Dashboard
|
||||
</Title>
|
||||
<Box className="flex gap-2">
|
||||
<Button onClick={handleResetTool} leftSection={<IconRestore size={16} />} size="xs">
|
||||
Reset tool
|
||||
</Button>
|
||||
<Button onClick={handleShutdownTool} leftSection={<IconPower size={16} />} color="red" size="xs">
|
||||
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} />)}
|
||||
const handleResetTool = () => {
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
await resetTool();
|
||||
setLoading(false);
|
||||
},
|
||||
title: "Confirm tool reset",
|
||||
message:
|
||||
"Are you sure you want to reset this tool? All current processes will be stopped and restarted.",
|
||||
okButton: { value: "Ok", color: "blue" },
|
||||
});
|
||||
};
|
||||
|
||||
{workingData.length <= 0 && (
|
||||
<Box className="flex items-center justify-center col-span-4">
|
||||
<Text>No Pages</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
const handleShutdownTool = () => {
|
||||
setConfirm({
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
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" },
|
||||
});
|
||||
};
|
||||
|
||||
<LoadingOverlay visible={loading} />
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<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-explicit-any */
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import moment from 'moment';
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import moment from "moment";
|
||||
export function cn(...args: ClassValue[]) {
|
||||
return twMerge(clsx(args));
|
||||
return twMerge(clsx(args));
|
||||
}
|
||||
|
||||
export const formatTime = (time: string, patent = 'DD/MM/YYYY') => {
|
||||
return moment(time).format(patent);
|
||||
export const formatTime = (time: string, patent = "DD/MM/YYYY") => {
|
||||
return moment(time).format(patent);
|
||||
};
|
||||
|
||||
export function removeFalsyValues<T extends Record<string, any>>(obj: T, excludeKeys: (keyof T)[] = []): Partial<T> {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key as keyof T)) {
|
||||
acc[key as keyof T] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<T>);
|
||||
export function removeFalsyValues<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
excludeKeys: (keyof T)[] = []
|
||||
): Partial<T> {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key as keyof T)) {
|
||||
acc[key as keyof T] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<T>);
|
||||
}
|
||||
|
||||
export function isValidJSON(str: string): boolean {
|
||||
if (!str || str.length <= 0) return false;
|
||||
if (!str || str.length <= 0) return false;
|
||||
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSON.parse(str);
|
||||
return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function copyToClipboard(text: string, onSuccess?: () => void): void {
|
||||
if (!navigator.clipboard) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
if (!navigator.clipboard) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (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));
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (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));
|
||||
}
|
||||
}
|
||||
|
||||
export function base64ToFile(base64String: string, fileName: string): File {
|
||||
const [header, base64Content] = base64String.split(',');
|
||||
const [header, base64Content] = base64String.split(",");
|
||||
|
||||
const mimeTypeMatch = header.match(/:(.*?);/);
|
||||
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
|
||||
throw new Error('Invalid base64 string');
|
||||
}
|
||||
const mimeType = mimeTypeMatch[1];
|
||||
const mimeTypeMatch = header.match(/:(.*?);/);
|
||||
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
|
||||
throw new Error("Invalid base64 string");
|
||||
}
|
||||
const mimeType = mimeTypeMatch[1];
|
||||
|
||||
const binaryString = atob(base64Content);
|
||||
const binaryString = atob(base64Content);
|
||||
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
byteArray[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; 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 {
|
||||
if (typeof str !== 'string') return ''; // Kiểm tra giá trị đầu vào
|
||||
if (typeof str !== "string") return ""; // Kiểm tra giá trị đầu vào
|
||||
|
||||
// Kiểm tra nếu môi trường hỗ trợ `normalize`
|
||||
const normalizedStr = str.normalize ? str.normalize('NFD') : str;
|
||||
// Kiểm tra nếu môi trường hỗ trợ `normalize`
|
||||
const normalizedStr = str.normalize ? str.normalize("NFD") : str;
|
||||
|
||||
return normalizedStr
|
||||
.replace(/[\u0300-\u036f]/g, '') // Xóa dấu
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, '') // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
|
||||
.trim() // Xóa khoảng trắng đầu/cuối
|
||||
.replace(/\s+/g, '-') // Thay khoảng trắng bằng "-"
|
||||
.replace(/-+/g, '-') // Gộp nhiều dấu "-" thành 1
|
||||
.toLowerCase() // Chuyển về chữ thường
|
||||
.slice(0, maxLength) // Giới hạn độ dài
|
||||
.replace(/^-+|-+$/g, ''); // Xóa "-" đầu/cuối
|
||||
return normalizedStr
|
||||
.replace(/[\u0300-\u036f]/g, "") // Xóa dấu
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "") // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
|
||||
.trim() // Xóa khoảng trắng đầu/cuối
|
||||
.replace(/\s+/g, "-") // Thay khoảng trắng bằng "-"
|
||||
.replace(/-+/g, "-") // Gộp nhiều dấu "-" thành 1
|
||||
.toLowerCase() // Chuyển về chữ thường
|
||||
.slice(0, maxLength) // Giới hạn độ dài
|
||||
.replace(/^-+|-+$/g, ""); // Xóa "-" đầu/cuối
|
||||
}
|
||||
|
||||
export function estimateReadingTimeInSeconds(content: string, wordsPerMinute = 200): number {
|
||||
if (!content || typeof content !== 'string') return 0;
|
||||
export function estimateReadingTimeInSeconds(
|
||||
content: string,
|
||||
wordsPerMinute = 200
|
||||
): number {
|
||||
if (!content || typeof content !== "string") return 0;
|
||||
|
||||
const wordCount = content.trim().split(/\s+/).length;
|
||||
return Math.ceil((wordCount / wordsPerMinute) * 60);
|
||||
const wordCount = content.trim().split(/\s+/).length;
|
||||
return Math.ceil((wordCount / wordsPerMinute) * 60);
|
||||
}
|
||||
|
||||
export function extractDomain(url: string): string | null {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error) {
|
||||
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 AppResponse from 'src/response/app-response';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import { BidsService } from '../services/bids.service';
|
||||
|
||||
@Injectable()
|
||||
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) {
|
||||
return AppResponse.toResponse([]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Post } from '@nestjs/common';
|
||||
import { Controller, Get, Post } from '@nestjs/common';
|
||||
import { DashboardService } from '../../services/dashboard.service';
|
||||
|
||||
@Controller('admin/dashboards')
|
||||
|
|
@ -14,4 +14,9 @@ export class AdminDashboardController {
|
|||
async shutdownTool() {
|
||||
return await this.dashboardService.shutdownTool();
|
||||
}
|
||||
|
||||
@Get('status-tool')
|
||||
async statusTool() {
|
||||
return await this.dashboardService.statusTool();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { BidsService } from '../../services/bids.service';
|
|||
import { WebBidsService } from '../../services/web-bids.service';
|
||||
import { Event } from '../../utils/events';
|
||||
import AppResponse from '@/response/app-response';
|
||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
|
||||
|
||||
@Controller('bids')
|
||||
export class BidsController {
|
||||
|
|
@ -68,17 +69,24 @@ export class BidsController {
|
|||
return this.bidsService.updateStatusWork(id, type, image);
|
||||
}
|
||||
|
||||
@Post('update-login-status')
|
||||
async updateLoginStatus(
|
||||
@Body() data: ClientUpdateLoginStatusDto
|
||||
) {
|
||||
return await this.bidsService.emitLoginStatus(data)
|
||||
}
|
||||
|
||||
@Post('test')
|
||||
async test(@Body('code') code: string) {
|
||||
const webBid = await this.webBidService.webBidRepo.findOne({
|
||||
// where: { id: 9 },
|
||||
where: { id: 8 },
|
||||
where: { id: 4 },
|
||||
// where: { id: 1 },
|
||||
});
|
||||
|
||||
this.eventEmitter.emit(Event.verifyCode(webBid), {
|
||||
code,
|
||||
// name: 'LAWSONS',
|
||||
name: 'LANGTONS',
|
||||
name: 'LAWSONS',
|
||||
// name: 'LANGTONS',
|
||||
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.eventEmitter.onAny(
|
||||
(
|
||||
event: string,
|
||||
payload: { login_status: string; data: WebBid },
|
||||
) => {
|
||||
if (!event.startsWith(Event.LOGIN_STATUS)) return;
|
||||
|
||||
this.server.emit(Event.statusLogin(payload.data), payload);
|
||||
},
|
||||
);
|
||||
|
||||
// IMAP
|
||||
this.imapService.connectIMAP();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { WebBidsService } from './web-bids.service';
|
|||
import { NotificationService } from '@/modules/notification/notification.service';
|
||||
import { Event } from '../utils/events';
|
||||
import _ from 'lodash';
|
||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BidsService {
|
||||
|
|
@ -273,7 +274,7 @@ export class BidsService {
|
|||
const result = await this.bidsRepo.save({
|
||||
...bid,
|
||||
...data,
|
||||
current_price: Math.max(data.current_price, bid.current_price),
|
||||
current_price: Math.max(data?.current_price || 0, bid.current_price),
|
||||
updated_at: new Date(), // Cập nhật timestamp
|
||||
});
|
||||
|
||||
|
|
@ -508,4 +509,12 @@ export class BidsService {
|
|||
|
||||
return AppResponse.toResponse(files);
|
||||
}
|
||||
|
||||
async emitLoginStatus(data: ClientUpdateLoginStatusDto){
|
||||
|
||||
this.eventEmitter.emit(Event.statusLogin(data.data), data)
|
||||
|
||||
|
||||
return AppResponse.toResponse(true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export class DashboardService {
|
|||
|
||||
private readonly tool_name = 'auto-bid-tool';
|
||||
|
||||
async resetToolByName(toolName: string): Promise<string> {
|
||||
async resetProcessByName(toolName: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Lấy danh sách process đang chạy
|
||||
exec('pm2 jlist', (error, stdout, stderr) => {
|
||||
|
|
@ -41,7 +41,35 @@ export class DashboardService {
|
|||
});
|
||||
}
|
||||
|
||||
async shutdownToolByName(toolName: string): Promise<string> {
|
||||
async getStatusProcessByName(toolName: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('pm2 jlist', (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
return reject(`Error get list process: ${stderr}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const processList = JSON.parse(stdout);
|
||||
const targetProcess = processList.find(
|
||||
(proc: any) => proc.name === toolName,
|
||||
);
|
||||
|
||||
if (!targetProcess) {
|
||||
return reject(`Not found process for name "${toolName}"`);
|
||||
}
|
||||
|
||||
const status = targetProcess.pm2_env?.status || 'unknown';
|
||||
return resolve(status); // Trả về: 'online', 'stopped', 'errored', etc.
|
||||
|
||||
} catch (parseErr) {
|
||||
reject(`Error parse JSON output: ${parseErr}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async shutdownProcessByName(toolName: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Lấy danh sách process đang chạy
|
||||
exec('pm2 jlist', (error, stdout, stderr) => {
|
||||
|
|
@ -77,7 +105,7 @@ export class DashboardService {
|
|||
|
||||
async resetTool() {
|
||||
try {
|
||||
await this.resetToolByName(this.tool_name);
|
||||
await this.resetProcessByName(this.tool_name);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
} catch (error) {
|
||||
|
|
@ -87,11 +115,22 @@ export class DashboardService {
|
|||
|
||||
async shutdownTool() {
|
||||
try {
|
||||
await this.shutdownToolByName(this.tool_name);
|
||||
await this.shutdownProcessByName(this.tool_name);
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
} catch (error) {
|
||||
return AppResponse.toResponse(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async statusTool() {
|
||||
try {
|
||||
const result = await this.getStatusProcessByName(this.tool_name);
|
||||
|
||||
return AppResponse.toResponse(result);
|
||||
} catch (error) {
|
||||
return AppResponse.toResponse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@ export class Event {
|
|||
public static BIDS_UPDATED = 'bidsUpdated';
|
||||
public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
|
||||
public static WEB_UPDATED = 'webUpdated';
|
||||
public static LOGIN_STATUS = 'login-status';
|
||||
|
||||
public static verifyCode(data: WebBid) {
|
||||
return `${this.VERIFY_CODE}.${data.origin_url}`;
|
||||
}
|
||||
|
||||
public static statusLogin(data: WebBid) {
|
||||
return `${this.LOGIN_STATUS}.${data.origin_url}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,21 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_NAME'),
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
entities: ['dist/**/*.entity{.ts,.js}'],
|
||||
synchronize:
|
||||
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
|
||||
}),
|
||||
useFactory: (configService: ConfigService) => {
|
||||
|
||||
return {
|
||||
type: 'mysql',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_NAME'),
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
entities: ['dist/**/*.entity{.ts,.js}'],
|
||||
synchronize:
|
||||
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export function extractModelId(url: string): string | null {
|
|||
const match = url.split('_');
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case 'https://www.pickles.com.au': {
|
||||
const model = url.split('/').pop();
|
||||
return model ? model : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "auto-bid-tool",
|
||||
script: "./index.js",
|
||||
instances: 1,
|
||||
exec_mode: "fork",
|
||||
watch: false,
|
||||
log_date_format: "YYYY-MM-DD HH:mm:ss",
|
||||
output: "./logs/out.log",
|
||||
error: "./logs/error.log",
|
||||
merge_logs: true,
|
||||
max_memory_restart: "12G",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
import 'dotenv/config';
|
||||
import _ from 'lodash';
|
||||
import pLimit from 'p-limit';
|
||||
import { io } from 'socket.io-client';
|
||||
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js';
|
||||
import browser from './system/browser.js';
|
||||
import configs from './system/config.js';
|
||||
import { delay, isTimeReached, safeClosePage } from './system/utils.js';
|
||||
import "dotenv/config";
|
||||
import _ from "lodash";
|
||||
import pLimit from "p-limit";
|
||||
import { io } from "socket.io-client";
|
||||
import {
|
||||
createApiBid,
|
||||
createBidProduct,
|
||||
deleteProfile,
|
||||
shouldUpdateProductTab,
|
||||
} from "./service/app-service.js";
|
||||
import browser from "./system/browser.js";
|
||||
import configs from "./system/config.js";
|
||||
import { delay, isTimeReached, safeClosePage } from "./system/utils.js";
|
||||
import { updateLoginStatus } from "./system/apis/bid.js";
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
|
|
@ -14,260 +20,335 @@ let MANAGER_BIDS = [];
|
|||
const activeTasks = new Set();
|
||||
|
||||
const handleUpdateProductTabs = (data) => {
|
||||
if (!Array.isArray(data)) {
|
||||
console.log('Data must be array');
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data)) {
|
||||
console.log("Data must be array");
|
||||
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 prevApiBid = managerBidMap.get(web.id);
|
||||
const newDataManager = data.map(({ children, ...web }) => {
|
||||
const prevApiBid = managerBidMap.get(web.id);
|
||||
|
||||
const newChildren = children.map((item) => {
|
||||
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
|
||||
const newChildren = children.map((item) => {
|
||||
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
|
||||
|
||||
if (prevProductTab) {
|
||||
prevProductTab.setNewData(item);
|
||||
if (prevProductTab) {
|
||||
prevProductTab.setNewData(item);
|
||||
|
||||
return prevProductTab;
|
||||
}
|
||||
return prevProductTab;
|
||||
}
|
||||
|
||||
return createBidProduct(web, item);
|
||||
});
|
||||
|
||||
if (prevApiBid) {
|
||||
prevApiBid.setNewData({ children: newChildren, ...web });
|
||||
return prevApiBid;
|
||||
}
|
||||
|
||||
return createApiBid({ ...web, children: newChildren });
|
||||
return createBidProduct(web, item);
|
||||
});
|
||||
|
||||
MANAGER_BIDS = newDataManager;
|
||||
if (prevApiBid) {
|
||||
prevApiBid.setNewData({ children: newChildren, ...web });
|
||||
return prevApiBid;
|
||||
}
|
||||
|
||||
return createApiBid({ ...web, children: newChildren });
|
||||
});
|
||||
|
||||
MANAGER_BIDS = newDataManager;
|
||||
};
|
||||
|
||||
const tracking = async () => {
|
||||
console.log('🚀 Tracking process started...');
|
||||
console.log("🚀 Tracking process started...");
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
console.log('🔍 Scanning active bids...');
|
||||
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
|
||||
while (true) {
|
||||
try {
|
||||
console.log("🔍 Scanning active bids...");
|
||||
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
||||
|
||||
await Promise.allSettled(
|
||||
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
||||
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
||||
return apiBid.listen_events();
|
||||
}),
|
||||
await Promise.allSettled(
|
||||
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
||||
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
||||
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(
|
||||
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();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}),
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Dọn dẹp tab không dùng
|
||||
console.log('🧹 Cleaning up unused tabs...');
|
||||
clearLazyTab();
|
||||
// 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.`
|
||||
);
|
||||
}
|
||||
|
||||
// Cập nhật trạng thái tracking
|
||||
console.log('📊 Tracking work status...');
|
||||
workTracking();
|
||||
} catch (error) {
|
||||
console.error('❌ Error in tracking loop:', error);
|
||||
}
|
||||
// Chờ first bid
|
||||
if (!productTab.first_bid) {
|
||||
console.log(
|
||||
`🎯 Waiting for first bid for Product ID: ${productTab.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
|
||||
await delay(configs.AUTO_TRACKING_DELAY);
|
||||
// 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();
|
||||
})
|
||||
);
|
||||
|
||||
// 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 () => {
|
||||
if (!global.IS_CLEANING) {
|
||||
console.log('🚀 Cleaning flag is OFF. Proceeding with operation.');
|
||||
return;
|
||||
}
|
||||
if (!global.IS_CLEANING) {
|
||||
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
console.warn('⚠️ Browser is not available or disconnected.');
|
||||
return;
|
||||
}
|
||||
if (!browser) {
|
||||
console.warn("⚠️ Browser is not available or disconnected.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pages = await browser.pages();
|
||||
try {
|
||||
const pages = await browser.pages();
|
||||
|
||||
// Lấy danh sách URL từ flattenedArray
|
||||
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
// Lấy danh sách URL từ flattenedArray
|
||||
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||
item.url,
|
||||
...item.children.map((child) => child.url),
|
||||
]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
|
||||
console.log(
|
||||
'🔍 Page URLs:',
|
||||
pages.map((page) => page.url()),
|
||||
);
|
||||
console.log(
|
||||
"🔍 Page URLs:",
|
||||
pages.map((page) => page.url())
|
||||
);
|
||||
|
||||
for (const page of pages) {
|
||||
const pageUrl = page.url();
|
||||
for (const page of pages) {
|
||||
const pageUrl = page.url();
|
||||
|
||||
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
|
||||
if (!pageUrl || pageUrl === 'about:blank') continue;
|
||||
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
|
||||
if (!pageUrl || pageUrl === "about:blank") continue;
|
||||
|
||||
if (!activeUrls.includes(pageUrl)) {
|
||||
if (!page.isClosed() && browser.isConnected()) {
|
||||
try {
|
||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
.map((i) => ({
|
||||
current_url: i.page_context.url(),
|
||||
data: i,
|
||||
}))
|
||||
.find((j) => j.current_url === pageUrl);
|
||||
if (!activeUrls.includes(pageUrl)) {
|
||||
if (!page.isClosed() && browser.isConnected()) {
|
||||
try {
|
||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
.map((i) => ({
|
||||
current_url: i.page_context.url(),
|
||||
data: i,
|
||||
}))
|
||||
.find((j) => j.current_url === pageUrl);
|
||||
|
||||
console.log(bidData);
|
||||
console.log(bidData);
|
||||
|
||||
if (bidData && bidData.data) {
|
||||
await safeClosePage(bidData.data);
|
||||
} else {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
|
||||
}
|
||||
}
|
||||
if (bidData && bidData.data) {
|
||||
await safeClosePage(bidData.data);
|
||||
} else {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error in clearLazyTab:', err.message);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("❌ Error in clearLazyTab:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const workTracking = async () => {
|
||||
try {
|
||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
||||
const limit = pLimit(5);
|
||||
try {
|
||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [
|
||||
item,
|
||||
...item.children,
|
||||
]);
|
||||
const limit = pLimit(5);
|
||||
|
||||
await Promise.allSettled(
|
||||
activeData
|
||||
.filter((item) => item.page_context && !item.page_context.isClosed())
|
||||
.filter((item) => !activeTasks.has(item.id))
|
||||
.map((item) =>
|
||||
limit(async () => {
|
||||
activeTasks.add(item.id);
|
||||
try {
|
||||
await item.handleTakeWorkSnapshot();
|
||||
} catch (error) {
|
||||
console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error);
|
||||
} finally {
|
||||
activeTasks.delete(item.id);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack);
|
||||
await Promise.allSettled(
|
||||
activeData
|
||||
.filter((item) => item.page_context && !item.page_context.isClosed())
|
||||
.filter((item) => !activeTasks.has(item.id))
|
||||
.map((item) =>
|
||||
limit(async () => {
|
||||
activeTasks.add(item.id);
|
||||
try {
|
||||
await item.handleTakeWorkSnapshot();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`,
|
||||
error
|
||||
);
|
||||
} finally {
|
||||
activeTasks.delete(item.id);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[❌ ERROR] Work tracking failed: ${error.message}\n`,
|
||||
error.stack
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const trackingLoginStatus = async () => {
|
||||
try {
|
||||
if (!MANAGER_BIDS?.length) return;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
MANAGER_BIDS.map(async (item) => {
|
||||
try {
|
||||
const login_status = await item.isLogin();
|
||||
|
||||
await updateLoginStatus({
|
||||
data: {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
origin_url: item.origin_url,
|
||||
},
|
||||
login_status,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[⚠️ WARN] Failed to check login for bid ${
|
||||
item?.id || "unknown"
|
||||
}: ${err.message}`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Optional: log summary
|
||||
const failed = results.filter((r) => r.status === "rejected").length;
|
||||
if (failed) {
|
||||
console.warn(`[⚠️ WARN] ${failed} login status checks failed.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[❌ ERROR] Login status tracking failed: ${error.message}\n`,
|
||||
error.stack
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
||||
transports: ['websocket'],
|
||||
reconnection: true,
|
||||
extraHeaders: {
|
||||
Authorization: process.env.CLIENT_KEY,
|
||||
},
|
||||
});
|
||||
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
||||
transports: ["websocket"],
|
||||
reconnection: true,
|
||||
extraHeaders: {
|
||||
Authorization: process.env.CLIENT_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
// set socket on global app
|
||||
global.socket = socket;
|
||||
// set socket on global app
|
||||
global.socket = socket;
|
||||
|
||||
// listen connect
|
||||
socket.on('connect', () => {
|
||||
console.log('✅ Connected to WebSocket server');
|
||||
console.log('🔗 Socket ID:', socket.id);
|
||||
});
|
||||
// listen connect
|
||||
socket.on("connect", () => {
|
||||
console.log("✅ Connected to WebSocket server");
|
||||
console.log("🔗 Socket ID:", socket.id);
|
||||
});
|
||||
|
||||
// listen connect
|
||||
socket.on('disconnect', () => {
|
||||
console.log('❌Client key is valid. Disconnected');
|
||||
});
|
||||
// listen connect
|
||||
socket.on("disconnect", () => {
|
||||
console.log("❌Client key is valid. Disconnected");
|
||||
});
|
||||
|
||||
// listen event
|
||||
socket.on('bidsUpdated', async (data) => {
|
||||
console.log('📢 Bids Data:', data);
|
||||
// listen event
|
||||
socket.on("bidsUpdated", async (data) => {
|
||||
console.log("📢 Bids Data:", data);
|
||||
|
||||
handleUpdateProductTabs(data);
|
||||
});
|
||||
handleUpdateProductTabs(data);
|
||||
});
|
||||
|
||||
socket.on('webUpdated', async (data) => {
|
||||
console.log('📢 Account was updated:', data);
|
||||
socket.on("webUpdated", async (data) => {
|
||||
console.log("📢 Account was updated:", data);
|
||||
|
||||
const isDeleted = deleteProfile(data);
|
||||
const isDeleted = deleteProfile(data);
|
||||
|
||||
if (isDeleted) {
|
||||
console.log('✅ Profile deleted successfully!');
|
||||
if (isDeleted) {
|
||||
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;
|
||||
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
|
||||
global.IS_CLEANING = false;
|
||||
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
|
||||
|
||||
await safeClosePage(tab);
|
||||
await safeClosePage(tab);
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
} else {
|
||||
console.log('⚠️ No profile found to delete.');
|
||||
}
|
||||
});
|
||||
global.IS_CLEANING = true;
|
||||
} else {
|
||||
console.log("⚠️ No profile found to delete.");
|
||||
}
|
||||
});
|
||||
|
||||
// AUTO TRACKING
|
||||
tracking();
|
||||
// AUTO TRACKING
|
||||
tracking();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,30 +1,44 @@
|
|||
import BID_TYPE from '../system/bid-type.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { takeSnapshot } from '../system/utils.js';
|
||||
import _ from 'lodash';
|
||||
import BID_TYPE from "../system/bid-type.js";
|
||||
import CONSTANTS from "../system/constants.js";
|
||||
import { takeSnapshot } from "../system/utils.js";
|
||||
import _ from "lodash";
|
||||
|
||||
export class Bid {
|
||||
type;
|
||||
puppeteer_connect;
|
||||
url;
|
||||
action;
|
||||
page_context;
|
||||
type;
|
||||
puppeteer_connect;
|
||||
url;
|
||||
action;
|
||||
page_context;
|
||||
|
||||
constructor(type, url, puppeteer_connect) {
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
this.puppeteer_connect = puppeteer_connect;
|
||||
constructor(type, url, puppeteer_connect) {
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
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 () => {
|
||||
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);
|
||||
async isLogin() {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,226 +1,292 @@
|
|||
import path from 'path';
|
||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
||||
import configs from '../../system/config.js';
|
||||
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
|
||||
import { ApiBid } from '../api-bid.js';
|
||||
import fs from 'fs';
|
||||
import path from "path";
|
||||
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
|
||||
import configs from "../../system/config.js";
|
||||
import {
|
||||
delay,
|
||||
extractNumber,
|
||||
getPathProfile,
|
||||
isTimeReached,
|
||||
safeClosePage,
|
||||
} from "../../system/utils.js";
|
||||
import { ApiBid } from "../api-bid.js";
|
||||
import fs from "fs";
|
||||
|
||||
export class GrayApiBid extends ApiBid {
|
||||
retry_login = 0;
|
||||
retry_login_count = 3;
|
||||
retry_login = 0;
|
||||
retry_login_count = 3;
|
||||
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
}
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
}
|
||||
|
||||
async polling(page) {
|
||||
try {
|
||||
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
|
||||
// page.removeAllListeners('request');
|
||||
// await page.setRequestInterception(true);
|
||||
async polling(page) {
|
||||
try {
|
||||
// // 🔥 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('api/Notifications/GetOutBidLots')) {
|
||||
// console.log('🚀 Fake response cho request:', request.url());
|
||||
// page.on('request', (request) => {
|
||||
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
|
||||
// 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({
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
console.log(`🔄 [${this.id}] Starting polling process...`);
|
||||
console.log(`🔄 [${this.id}] Starting polling process...`);
|
||||
|
||||
await page.evaluateHandle(
|
||||
(apiUrl, interval, bidId) => {
|
||||
if (window._autoBidPollingStarted) {
|
||||
console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`);
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
await page.evaluateHandle(
|
||||
(apiUrl, interval, bidId) => {
|
||||
if (window._autoBidPollingStarted) {
|
||||
console.log(
|
||||
`✅ [${bidId}] Polling is already running. Skipping initialization.`
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||
console.log(`🚀 [${bidId}] Initializing polling...`);
|
||||
window._autoBidPollingStarted = true;
|
||||
|
||||
try {
|
||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||
await page.click('#loginButton');
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
|
||||
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
|
||||
]);
|
||||
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);
|
||||
}
|
||||
|
||||
if (!(await page.$('input[name="username"]'))) {
|
||||
console.log(`✅ [${this.id}] Login successful!`);
|
||||
this.retry_login = 0; // Reset retry count after success
|
||||
return;
|
||||
}
|
||||
console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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} ❌`);
|
||||
async handleCreateLogsOnServer(data) {
|
||||
if (!Array.isArray(data)) return;
|
||||
|
||||
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;
|
||||
}
|
||||
const values = data.map((item) => {
|
||||
return {
|
||||
model: item.Sku,
|
||||
lot_id: item.Id,
|
||||
out_price: extractNumber(item.Bid) || 0,
|
||||
raw_data: JSON.stringify(item),
|
||||
};
|
||||
});
|
||||
|
||||
safeClosePage(this); // Close the current page
|
||||
await delay(1000);
|
||||
await createOutBidLog(values);
|
||||
}
|
||||
|
||||
if (!this.page_context) {
|
||||
await this.puppeteer_connect(); // Reconnect if page is closed
|
||||
}
|
||||
listen_out_bids = async (data) => {
|
||||
if (this.children.length <= 0 || data.length <= 0) return;
|
||||
|
||||
return await this.action(); // Retry login
|
||||
} finally {
|
||||
global.IS_CLEANING = true;
|
||||
}
|
||||
// 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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
return false;
|
||||
};
|
||||
|
||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
||||
console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
|
||||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
|
||||
await page.bringToFront();
|
||||
console.log(`🎯 [${this.id}] Brought page to front.`);
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
// 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.`);
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
page.on('response', async (response) => {
|
||||
if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
|
||||
console.log(`🚀 [${this.id}] API POST detected: ${response.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.`);
|
||||
|
||||
try {
|
||||
const responseBody = await response.json();
|
||||
await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
|
||||
} catch (error) {
|
||||
console.error(`❌ [${this.id}] Error processing response:`, error?.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
global.IS_CLEANING = true;
|
||||
this.retry_login = 0; // Reset retry count
|
||||
return;
|
||||
}
|
||||
|
||||
page.on('load', async () => {
|
||||
console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
|
||||
await this.polling(page);
|
||||
await this.handleLogin();
|
||||
});
|
||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||
|
||||
await this.polling(page); // Call when first load
|
||||
await this.handleLogin();
|
||||
} catch (error) {
|
||||
console.log(`❌ [${this.id}] Action error: ${error.message}`);
|
||||
try {
|
||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||
await page.click("#loginButton");
|
||||
|
||||
await 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 CONSTANTS from '../../system/constants.js';
|
||||
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
||||
import { ProductBid } from '../product-bid.js';
|
||||
import {
|
||||
outBid,
|
||||
pushPrice,
|
||||
updateBid,
|
||||
updateStatusByPrice,
|
||||
} from "../../system/apis/bid.js";
|
||||
import CONSTANTS from "../../system/constants.js";
|
||||
import {
|
||||
delay,
|
||||
extractNumber,
|
||||
isNumber,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
safeClosePage,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
export class GraysProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
constructor({ ...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 (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
|
||||
console.log(`❌ [${this.id}] It's not time yet`);
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
if (!isNumber(price_value)) {
|
||||
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
|
||||
await takeSnapshot(page, this, "price-value-null");
|
||||
|
||||
if (!isNumber(price_value)) {
|
||||
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 };
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
getCloseTime = async () => {
|
||||
try {
|
||||
if (!this.page_context) return null;
|
||||
const bid_price = this.plus_price + Number(price_value);
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
await outBid(this.id);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
async handleWritePrice(page, bid_price) {
|
||||
await page.type('#price', String(bid_price));
|
||||
await delay(500);
|
||||
const response = await pushPrice({
|
||||
bid_id: this.id,
|
||||
price: bid_price,
|
||||
});
|
||||
|
||||
if (!response.status) {
|
||||
return { result: false, bid_price: 0 };
|
||||
}
|
||||
|
||||
async placeBid(page) {
|
||||
try {
|
||||
await page.click('#bid-type-standard');
|
||||
await delay(500);
|
||||
this.histories = response.data;
|
||||
|
||||
await page.click('#btnSubmit');
|
||||
await delay(1000);
|
||||
|
||||
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;
|
||||
}
|
||||
// RESET first bid
|
||||
if (this.histories.length > 0 && this.first_bid) {
|
||||
this.first_bid = false;
|
||||
}
|
||||
|
||||
async handleReturnProductPage(page) {
|
||||
await page.goto(this.url);
|
||||
await delay(1000);
|
||||
return { result: true, bid_price };
|
||||
}
|
||||
|
||||
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 }) {
|
||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
||||
await delay(500);
|
||||
|
||||
if (response) {
|
||||
this.lot_id = response.lot_id;
|
||||
this.close_time = response.close_time;
|
||||
this.start_bid_time = response.start_bid_time;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
update = async () => {
|
||||
if (!this.page_context) return;
|
||||
return { result: false, close_time };
|
||||
}
|
||||
|
||||
const page = this.page_context;
|
||||
async handleWritePrice(page, bid_price) {
|
||||
await page.type("#price", String(bid_price));
|
||||
await delay(500);
|
||||
}
|
||||
|
||||
try {
|
||||
const close_time = await this.getCloseTime();
|
||||
async placeBid(page) {
|
||||
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.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
|
||||
const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
|
||||
await page.click("#btnSubmit");
|
||||
await delay(1000);
|
||||
|
||||
await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
|
||||
const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null);
|
||||
await page.waitForSelector("button", { timeout: 5000 });
|
||||
|
||||
await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null);
|
||||
const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
|
||||
await delay(500);
|
||||
|
||||
await page
|
||||
.waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 })
|
||||
.catch(() => null);
|
||||
const current_price = await page
|
||||
.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
|
||||
.catch(() => null);
|
||||
await page.click("button");
|
||||
|
||||
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(
|
||||
{
|
||||
lot_id,
|
||||
reserve_price: price_value,
|
||||
close_time: close_time ? String(close_time) : null,
|
||||
name,
|
||||
current_price: current_price ? extractNumber(current_price) : null,
|
||||
},
|
||||
['close_time'],
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.handleUpdateBid(data);
|
||||
async handleReturnProductPage(page) {
|
||||
await page.goto(this.url);
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
return { price_value, lot_id, name, current_price };
|
||||
} catch (error) {
|
||||
console.error(`🚨 Error updating product info: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
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,
|
||||
});
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
if (response) {
|
||||
this.lot_id = response.lot_id;
|
||||
this.close_time = response.close_time;
|
||||
this.start_bid_time = response.start_bid_time;
|
||||
}
|
||||
}
|
||||
|
||||
await this.gotoLink();
|
||||
console.log(`🌍 [${this.id}] Navigated to link.`);
|
||||
update = async () => {
|
||||
if (!this.page_context) return;
|
||||
|
||||
await delay(1000);
|
||||
const page = this.page_context;
|
||||
|
||||
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
|
||||
if (isCloseProduct.result) {
|
||||
console.log(`❌ [${this.id}] The product is closed, cannot place a bid.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const close_time = await this.getCloseTime();
|
||||
|
||||
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();
|
||||
if (!price_value) return;
|
||||
await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
|
||||
const lot_id = await page
|
||||
.$eval("#lotId", (el) => el.value)
|
||||
.catch(() => null);
|
||||
|
||||
const { result, bid_price } = await this.validate({ page, price_value });
|
||||
if (!result) {
|
||||
console.log(`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`);
|
||||
return;
|
||||
}
|
||||
await page
|
||||
.waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
|
||||
timeout: 5000,
|
||||
})
|
||||
.catch(() => null);
|
||||
const name = await page
|
||||
.$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
|
||||
.catch(() => null);
|
||||
|
||||
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;
|
||||
}
|
||||
await page
|
||||
.waitForSelector(
|
||||
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.catch(() => null);
|
||||
const current_price = await page
|
||||
.$eval(
|
||||
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
|
||||
(el) => el.innerText
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
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}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
const data = removeFalsyValues(
|
||||
{
|
||||
lot_id,
|
||||
reserve_price: price_value,
|
||||
close_time: close_time ? String(close_time) : null,
|
||||
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}`);
|
||||
await this.handleReturnProductPage(page);
|
||||
} catch (error) {
|
||||
console.error(`🚨 [${this.id}] Error navigating the page: ${error.message}`);
|
||||
}
|
||||
};
|
||||
this.handleUpdateBid(data);
|
||||
|
||||
return { price_value, lot_id, name, current_price };
|
||||
} 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 configs from '../../system/config.js';
|
||||
import { getPathProfile, safeClosePage } from '../../system/utils.js';
|
||||
import { ApiBid } from '../api-bid.js';
|
||||
import _ from 'lodash';
|
||||
import { updateStatusByPrice } from '../../system/apis/bid.js';
|
||||
import fs from "fs";
|
||||
import configs from "../../system/config.js";
|
||||
import { getPathProfile, safeClosePage } from "../../system/utils.js";
|
||||
import { ApiBid } from "../api-bid.js";
|
||||
import _ from "lodash";
|
||||
import { updateStatusByPrice } from "../../system/apis/bid.js";
|
||||
|
||||
export class LangtonsApiBid extends ApiBid {
|
||||
reloadInterval = null;
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
reloadInterval = null;
|
||||
constructor({ ...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 () =>
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
const children = this.children.filter((item) => item.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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
console.log(
|
||||
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
||||
);
|
||||
await safeClosePage(this);
|
||||
}
|
||||
|
||||
async getWonList() {
|
||||
try {
|
||||
await page.waitForSelector('.row.account-product-list', { timeout: 30000 });
|
||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||
|
||||
const items = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('.row.account-product-list')).map((item) => item.getAttribute('data-lotid') || null);
|
||||
});
|
||||
try {
|
||||
// ⌨ Enter email
|
||||
console.log(`✍ [${this.id}] Entering email:`, this.username);
|
||||
await page.type('input[name="loginEmail"]', this.username, {
|
||||
delay: 100,
|
||||
});
|
||||
|
||||
return _.compact(items);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
// ⌨ 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() {
|
||||
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() {
|
||||
console.log(`🔄 [${this.id}] Starting to update the won list...`);
|
||||
// Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
|
||||
const result = _.filter(this.children, (item) =>
|
||||
_.includes(items, item.lot_id)
|
||||
);
|
||||
console.log(
|
||||
`✅ [${this.id}] ${result.length} items need to be updated:`,
|
||||
result
|
||||
);
|
||||
|
||||
// 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);
|
||||
// Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
|
||||
const responses = await Promise.allSettled(
|
||||
result.map((i) => updateStatusByPrice(i.id, i.current_price))
|
||||
);
|
||||
|
||||
// Nếu không có item nào, thoát ra
|
||||
if (items.length === 0) {
|
||||
console.log(`⚠️ [${this.id}] No items to update.`);
|
||||
return;
|
||||
// Log kết quả của mỗi request
|
||||
responses.forEach((response, index) => {
|
||||
if (response.status === "fulfilled") {
|
||||
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
|
||||
const result = _.filter(this.children, (item) => _.includes(items, item.lot_id));
|
||||
console.log(`✅ [${this.id}] ${result.length} items need to be updated:`, result);
|
||||
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||
|
||||
// Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
|
||||
const responses = await Promise.allSettled(result.map((i) => updateStatusByPrice(i.id, i.current_price)));
|
||||
await page.bringToFront();
|
||||
|
||||
// Log kết quả của mỗi request
|
||||
responses.forEach((response, index) => {
|
||||
if (response.status === 'fulfilled') {
|
||||
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;
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
listen_events = async () => {
|
||||
if (this.page_context) return;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
await this.puppeteer_connect();
|
||||
await this.action();
|
||||
|
||||
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();
|
||||
|
||||
// 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);
|
||||
// this.handleUpdateWonItem();
|
||||
} else {
|
||||
console.log(
|
||||
`❌ [${this.id}] Page context is closed. Stopping reload.`
|
||||
);
|
||||
clearInterval(this.reloadInterval);
|
||||
}
|
||||
};
|
||||
|
||||
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.`);
|
||||
|
||||
// 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
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
|
||||
}
|
||||
}, 60000); // 1p reload
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,409 +1,510 @@
|
|||
import _ from 'lodash';
|
||||
import { outBid, pushPrice, updateBid } from '../../system/apis/bid.js';
|
||||
import { sendMessage } from '../../system/apis/notification.js';
|
||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
||||
import configs from '../../system/config.js';
|
||||
import CONSTANTS from '../../system/constants.js';
|
||||
import { convertAETtoUTC, isTimeReached, removeFalsyValues, takeSnapshot } from '../../system/utils.js';
|
||||
import { ProductBid } from '../product-bid.js';
|
||||
import _ from "lodash";
|
||||
import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
|
||||
import { sendMessage } from "../../system/apis/notification.js";
|
||||
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
|
||||
import configs from "../../system/config.js";
|
||||
import CONSTANTS from "../../system/constants.js";
|
||||
import {
|
||||
convertAETtoUTC,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
takeSnapshot,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
export class LangtonsProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
constructor({ ...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
|
||||
async getCloseTime() {
|
||||
return new Promise((resolve) => {
|
||||
const onResponse = async (response) => {
|
||||
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;
|
||||
}
|
||||
|
||||
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.`);
|
||||
if (
|
||||
!response ||
|
||||
!response
|
||||
.request()
|
||||
.url()
|
||||
.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
|
||||
) {
|
||||
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: 21, //test
|
||||
// reserve_price: result.lotData?.minimumBid || null,
|
||||
// current_price: result.lotData?.currentMaxBid || null,
|
||||
current_price: 20, // test
|
||||
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
||||
close_time: close_time ? String(close_time) : null,
|
||||
name,
|
||||
},
|
||||
// [],
|
||||
['close_time'],
|
||||
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;
|
||||
}
|
||||
|
||||
// 📌 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
|
||||
await this.handleUpdateBid(data);
|
||||
async handlePlaceBid() {
|
||||
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!');
|
||||
|
||||
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');
|
||||
|
||||
return el;
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
async handlePlaceBid() {
|
||||
if (!this.page_context) {
|
||||
console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
|
||||
try {
|
||||
console.log(`🔄 [${this.id}] Starting 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;
|
||||
}
|
||||
const page = this.page_context;
|
||||
}
|
||||
|
||||
if (global[`IS_PLACE_BID-${this.id}`]) {
|
||||
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
|
||||
return;
|
||||
}
|
||||
console.log(`🔍 [${this.id}] Checking bid status...`);
|
||||
|
||||
try {
|
||||
console.log(`🔄 [${this.id}] Starting bid process...`);
|
||||
global[`IS_PLACE_BID-${this.id}`] = true;
|
||||
if (["Outbid"].includes(lotData?.bidStatus)) {
|
||||
console.log(
|
||||
`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`
|
||||
);
|
||||
|
||||
const continueShopBtn = await this.getContinueShopButton();
|
||||
if (continueShopBtn) {
|
||||
console.log(`⚠️ [${this.id}] Outbid detected, calling outBid function.`);
|
||||
await outBid(this.id);
|
||||
return;
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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".`);
|
||||
if (
|
||||
lotData.myBid &&
|
||||
this.max_price &&
|
||||
this.max_price != lotData.myBid
|
||||
) {
|
||||
this.handlePlaceBid();
|
||||
}
|
||||
} 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;
|
||||
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 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),
|
||||
};
|
||||
});
|
||||
action = async () => {
|
||||
try {
|
||||
const page = this.page_context;
|
||||
|
||||
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 { pushPrice, updateBid } from '../../system/apis/bid.js';
|
||||
import { sendMessage } from '../../system/apis/notification.js';
|
||||
import configs from '../../system/config.js';
|
||||
import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
|
||||
import { ProductBid } from '../product-bid.js';
|
||||
import _ from "lodash";
|
||||
import { pushPrice, updateBid } from "../../system/apis/bid.js";
|
||||
import { sendMessage } from "../../system/apis/notification.js";
|
||||
import configs from "../../system/config.js";
|
||||
import {
|
||||
delay,
|
||||
extractPriceNumber,
|
||||
isTimeReached,
|
||||
removeFalsyValues,
|
||||
} from "../../system/utils.js";
|
||||
import { ProductBid } from "../product-bid.js";
|
||||
|
||||
export class LawsonsProductBid extends ProductBid {
|
||||
constructor({ ...prev }) {
|
||||
super(prev);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
||||
async getReversePrice() {
|
||||
try {
|
||||
if (!this.page_context) return null;
|
||||
|
||||
if (response) {
|
||||
this.lot_id = response.lot_id;
|
||||
this.close_time = response.close_time;
|
||||
this.start_bid_time = response.start_bid_time;
|
||||
}
|
||||
await this.page_context.waitForSelector(
|
||||
".select-dropdown-value.text-truncate",
|
||||
{ timeout: 4000 }
|
||||
);
|
||||
const price = await this.page_context.evaluate(() => {
|
||||
const el = document.querySelector(
|
||||
".select-dropdown-value.text-truncate"
|
||||
);
|
||||
return el ? el.innerText : null;
|
||||
});
|
||||
|
||||
return price ? extractPriceNumber(price) : null;
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getReversePrice() {
|
||||
try {
|
||||
if (!this.page_context) return null;
|
||||
update = async () => {
|
||||
try {
|
||||
if (!this.page_context) return;
|
||||
|
||||
await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
|
||||
const price = await this.page_context.evaluate(() => {
|
||||
const el = document.querySelector('.select-dropdown-value.text-truncate');
|
||||
return el ? el.innerText : null;
|
||||
// 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 {
|
||||
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;
|
||||
} catch (error) {
|
||||
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;
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
|
||||
return await response.json();
|
||||
},
|
||||
this.max_price,
|
||||
this.model,
|
||||
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT
|
||||
);
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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;
|
||||
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 đấ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;
|
||||
// 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.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 {
|
||||
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;
|
||||
const result = await response.json();
|
||||
|
||||
// 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
|
||||
}
|
||||
if (!result) return;
|
||||
|
||||
// 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
|
||||
}
|
||||
console.log(`📈 [${this.id}] Bid data: `, result);
|
||||
|
||||
// Đợ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();
|
||||
const { maxBidAmount, currentBidAmount, isOutBid } = result;
|
||||
|
||||
// Lấy giá reserve price để kiểm tra
|
||||
const reservePrice = await this.getReversePrice();
|
||||
console.log(
|
||||
`📊 [${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á
|
||||
const shouldStop =
|
||||
!response ||
|
||||
response?.currentBidAmount > this.max_price + this.plus_price ||
|
||||
response.isOutBid != true ||
|
||||
!reservePrice ||
|
||||
reservePrice > this.max_price + this.plus_price;
|
||||
// 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}`);
|
||||
|
||||
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 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"
|
||||
}`
|
||||
);
|
||||
|
||||
// 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);
|
||||
if (!this.close_time || !this.lot_id || !this.current_price) return;
|
||||
|
||||
// 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(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 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 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 path from 'path';
|
||||
import BID_TYPE from '../system/bid-type.js';
|
||||
import browser from '../system/browser.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
import * as fs from "fs";
|
||||
import BID_TYPE from "../system/bid-type.js";
|
||||
import browser from "../system/browser.js";
|
||||
import { getPathProfile } from "../system/utils.js";
|
||||
import { Bid } from "./bid.js";
|
||||
|
||||
export class ProductBid extends Bid {
|
||||
id;
|
||||
max_price;
|
||||
model;
|
||||
lot_id;
|
||||
plus_price;
|
||||
close_time;
|
||||
first_bid;
|
||||
quantity;
|
||||
created_at;
|
||||
updated_at;
|
||||
histories;
|
||||
start_bid_time;
|
||||
parent_browser_context;
|
||||
web_bid;
|
||||
current_price;
|
||||
name;
|
||||
reserve_price;
|
||||
update;
|
||||
id;
|
||||
max_price;
|
||||
model;
|
||||
lot_id;
|
||||
plus_price;
|
||||
close_time;
|
||||
first_bid;
|
||||
quantity;
|
||||
created_at;
|
||||
updated_at;
|
||||
histories;
|
||||
start_bid_time;
|
||||
parent_browser_context;
|
||||
web_bid;
|
||||
current_price;
|
||||
name;
|
||||
reserve_price;
|
||||
update;
|
||||
|
||||
constructor({
|
||||
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,
|
||||
}) {
|
||||
super(BID_TYPE.PRODUCT_TAB, url);
|
||||
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.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
constructor({
|
||||
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,
|
||||
}) {
|
||||
super(BID_TYPE.PRODUCT_TAB, url);
|
||||
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.current_price = current_price;
|
||||
this.name = name;
|
||||
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({
|
||||
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;
|
||||
const context = await browser.createBrowserContext();
|
||||
|
||||
const statusInit = await this.restoreContext(context);
|
||||
|
||||
if (!statusInit) {
|
||||
console.log(`⚠️ Restore failed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
if (!this.parent_browser_context) {
|
||||
console.log(`❌ Connect fail. parent_browser_context is null: ${this.id}`);
|
||||
return;
|
||||
}
|
||||
const page = await context.newPage();
|
||||
|
||||
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) {
|
||||
console.log(`⚠️ Restore failed.`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
|
||||
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) {
|
||||
const filePath = getPathProfile(this.web_bid.origin_url);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
async gotoLink() {
|
||||
const page = this.page_context;
|
||||
|
||||
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
// Restore Cookies
|
||||
await context.setCookie(...contextData.cookies);
|
||||
|
||||
return true;
|
||||
if (page.isClosed()) {
|
||||
console.error("❌ Page has been closed, cannot navigate.");
|
||||
return;
|
||||
}
|
||||
|
||||
async gotoLink() {
|
||||
const page = this.page_context;
|
||||
console.log("🔄 Starting the bidding process...");
|
||||
|
||||
if (page.isClosed()) {
|
||||
console.error('❌ Page has been closed, cannot navigate.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await page.goto(this.url, { waitUntil: "networkidle2" });
|
||||
console.log(`✅ Navigated to: ${this.url}`);
|
||||
|
||||
console.log('🔄 Starting the bidding process...');
|
||||
|
||||
try {
|
||||
await page.goto(this.url, { waitUntil: 'networkidle2' });
|
||||
console.log(`✅ Navigated to: ${this.url}`);
|
||||
|
||||
await page.bringToFront();
|
||||
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
||||
console.log('👀 Brought the tab to the foreground.');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during navigation:', error);
|
||||
}
|
||||
await page.bringToFront();
|
||||
await page.setUserAgent(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
);
|
||||
console.log("👀 Brought the tab to the foreground.");
|
||||
} catch (error) {
|
||||
console.error("❌ Error during navigation:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,82 @@
|
|||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
|
||||
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
|
||||
import { LangtonsApiBid } from '../models/langtons.com.au/langtons-api-bid.js';
|
||||
import { LangtonsProductBid } from '../models/langtons.com.au/langtons-product-bid.js';
|
||||
import configs from '../system/config.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { sanitizeFileName } from '../system/utils.js';
|
||||
import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js';
|
||||
import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-product-bid.js';
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
|
||||
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
|
||||
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
|
||||
import { LangtonsProductBid } from "../models/langtons.com.au/langtons-product-bid.js";
|
||||
import { LawsonsApiBid } from "../models/lawsons.com.au/lawsons-api-bid.js";
|
||||
import { LawsonsProductBid } from "../models/lawsons.com.au/lawsons-product-bid.js";
|
||||
import { PicklesApiBid } from "../models/pickles.com.au/pickles-api-bid.js";
|
||||
import { PicklesProductBid } from "../models/pickles.com.au/pickles-product-bid.js";
|
||||
import configs from "../system/config.js";
|
||||
import CONSTANTS from "../system/constants.js";
|
||||
import { sanitizeFileName } from "../system/utils.js";
|
||||
|
||||
// Time to update
|
||||
const TIME = 30 * 1000;
|
||||
|
||||
export const handleCloseRemoveProduct = (data) => {
|
||||
if (!Array.isArray(data)) return;
|
||||
if (!Array.isArray(data)) return;
|
||||
|
||||
data.forEach(async (item) => {
|
||||
if (item.page_context) {
|
||||
safeClosePage(item);
|
||||
}
|
||||
});
|
||||
data.forEach(async (item) => {
|
||||
if (item.page_context) {
|
||||
safeClosePage(item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const createBidProduct = (web, data) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GraysProductBid({ ...data });
|
||||
}
|
||||
case configs.WEB_URLS.LANGTONS: {
|
||||
return new LangtonsProductBid({ ...data });
|
||||
}
|
||||
case configs.WEB_URLS.LAWSONS: {
|
||||
return new LawsonsProductBid({ ...data });
|
||||
}
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
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.PICKLES: {
|
||||
return new PicklesProductBid({ ...data });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createApiBid = (web) => {
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
return new GrayApiBid({ ...web });
|
||||
}
|
||||
case configs.WEB_URLS.LANGTONS: {
|
||||
return new LangtonsApiBid({ ...web });
|
||||
}
|
||||
case configs.WEB_URLS.LAWSONS: {
|
||||
return new LawsonsApiBid({ ...web });
|
||||
}
|
||||
switch (web.origin_url) {
|
||||
case configs.WEB_URLS.GRAYS: {
|
||||
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.PICKLES: {
|
||||
return new PicklesApiBid({ ...web });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteProfile = (data) => {
|
||||
if (!data?.origin_url) return false;
|
||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(data?.origin_url) + '.json');
|
||||
if (!data?.origin_url) return false;
|
||||
const filePath = path.join(
|
||||
CONSTANTS.PROFILE_PATH,
|
||||
sanitizeFileName(data?.origin_url) + ".json"
|
||||
);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
}
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const shouldUpdateProductTab = (productTab) => {
|
||||
const updatedAt = new Date(productTab.updated_at).getTime();
|
||||
const now = Date.now();
|
||||
const updatedAt = new Date(productTab.updated_at).getTime();
|
||||
const now = Date.now();
|
||||
|
||||
return now - updatedAt >= TIME;
|
||||
return now - updatedAt >= TIME;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,125 +1,143 @@
|
|||
import axios from '../axios.js';
|
||||
import fs from 'fs';
|
||||
import axios from "../axios.js";
|
||||
import fs from "fs";
|
||||
export const getBids = async () => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'GET',
|
||||
url: 'bids',
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "GET",
|
||||
url: "bids",
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ DATA IS NOT FOUND ON SERVER');
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER (GET BIDS): ', error);
|
||||
return [];
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ DATA IS NOT FOUND ON SERVER");
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER (GET BIDS): ", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const updateBid = async (id, values) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'PUT',
|
||||
url: 'bids/' + id,
|
||||
data: values,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "PUT",
|
||||
url: "bids/" + id,
|
||||
data: values,
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ UPDATE FAILURE (UPDATE BID)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
|
||||
return null;
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ UPDATE FAILURE (UPDATE BID)");
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER: (UPDATE BID) ", error.response);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const outBid = async (id) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'POST',
|
||||
url: 'bids/out-bid/' + id,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "POST",
|
||||
url: "bids/out-bid/" + id,
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ OUT BID UPDATE FAILURE');
|
||||
return false;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER (OUT BID UPDATE): ', error);
|
||||
return false;
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ OUT BID UPDATE FAILURE");
|
||||
return false;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER (OUT BID UPDATE): ", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const pushPrice = async (values) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'POST',
|
||||
url: 'bid-histories',
|
||||
data: values,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "POST",
|
||||
url: "bid-histories",
|
||||
data: values,
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ PUSH PRICE FAILURE');
|
||||
return { status: false, data: [] };
|
||||
}
|
||||
|
||||
return { status: true, data: data.data };
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error?.response);
|
||||
return { status: false, data: [] };
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ PUSH PRICE FAILURE");
|
||||
return { status: false, data: [] };
|
||||
}
|
||||
|
||||
return { status: true, data: data.data };
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER (PUSH PRICE): ", error?.response);
|
||||
return { status: false, data: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStatusByPrice = async (id, current_price) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'POST',
|
||||
url: 'bids/update-status/' + id,
|
||||
data: {
|
||||
current_price: Number(current_price) | 0,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "POST",
|
||||
url: "bids/update-status/" + id,
|
||||
data: {
|
||||
current_price: Number(current_price) | 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ UPDATE STATUS BY PRICE FAILURE');
|
||||
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: [] };
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ UPDATE STATUS BY PRICE FAILURE");
|
||||
return { status: false, data: [] };
|
||||
}
|
||||
|
||||
return { status: true, data: data.data };
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ", {
|
||||
// response: error.response,
|
||||
message: error.message,
|
||||
});
|
||||
return { status: false, data: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStatusWork = async (item, filePath) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
url: `bids/update-status-work/${item.type}/${item.id}`,
|
||||
data: {
|
||||
image: fs.createReadStream(filePath),
|
||||
},
|
||||
});
|
||||
fs.unlinkSync(filePath);
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
url: `bids/update-status-work/${item.type}/${item.id}`,
|
||||
data: {
|
||||
image: fs.createReadStream(filePath),
|
||||
},
|
||||
});
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
return response.data?.data;
|
||||
} catch (error) {
|
||||
console.error('❌ Upload failed:', error.response?.data || error.message);
|
||||
return false;
|
||||
}
|
||||
return response.data?.data;
|
||||
} catch (error) {
|
||||
console.error("❌ Upload failed:", error.response?.data || error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateLoginStatus = async (data) => {
|
||||
try {
|
||||
const response = await axios({
|
||||
method: "POST",
|
||||
url: `bids/update-login-status`,
|
||||
data,
|
||||
});
|
||||
|
||||
return response.data?.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ UPDATE FAILURE (LOGIN STATUS):",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import axios from '../axios.js';
|
||||
import axios from "../axios.js";
|
||||
|
||||
export const sendMessage = async (values) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: 'POST',
|
||||
url: 'notifications/send-messages',
|
||||
data: values,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
method: "POST",
|
||||
url: "notifications/send-messages",
|
||||
data: values,
|
||||
});
|
||||
|
||||
if (!data || !data?.data) {
|
||||
console.log('❌ UPDATE FAILURE (UPDATE Noti)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER: (UPDATE Noti) ', error);
|
||||
return null;
|
||||
if (!data || !data?.data) {
|
||||
console.log("❌ UPDATE FAILURE (UPDATE Noti)");
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log("❌ ERROR IN SERVER: (UPDATE Noti) ", error.response);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,34 +1,45 @@
|
|||
const configs = {
|
||||
AUTO_TRACKING_DELAY: 5000,
|
||||
AUTO_TRACKING_CLEANING: 10000,
|
||||
SOCKET_URL: process.env.SOCKET_URL,
|
||||
WEB_URLS: {
|
||||
GRAYS: `https://www.grays.com`,
|
||||
LANGTONS: `https://www.langtons.com.au`,
|
||||
LAWSONS: `https://www.lawsons.com.au`,
|
||||
AUTO_TRACKING_DELAY: 5000,
|
||||
AUTO_TRACKING_CLEANING: 10000,
|
||||
SOCKET_URL: process.env.SOCKET_URL,
|
||||
WEB_URLS: {
|
||||
GRAYS: `https://www.grays.com`,
|
||||
LANGTONS: `https://www.langtons.com.au`,
|
||||
LAWSONS: `https://www.lawsons.com.au`,
|
||||
PICKLES: `https://www.pickles.com.au`,
|
||||
},
|
||||
WEB_CONFIGS: {
|
||||
GRAYS: {
|
||||
AUTO_CALL_API_TO_TRACKING: 3000,
|
||||
API_CALL_TO_TRACKING:
|
||||
"https://www.grays.com/api/Notifications/GetOutBidLots",
|
||||
},
|
||||
WEB_CONFIGS: {
|
||||
GRAYS: {
|
||||
AUTO_CALL_API_TO_TRACKING: 3000,
|
||||
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
|
||||
},
|
||||
LANGTONS: {
|
||||
AUTO_CALL_API_TO_TRACKING: 5000,
|
||||
LOGIN_URL: 'https://www.langtons.com.au/account/login',
|
||||
API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
|
||||
},
|
||||
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',
|
||||
},
|
||||
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_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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue