update
This commit is contained in:
parent
efd8cd1fe8
commit
ec9b9506a6
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { handleError } from '.';
|
||||||
|
import axios from '../lib/axios';
|
||||||
|
import { IBid } from '../system/type';
|
||||||
|
|
||||||
|
export const getDetailBidHistories = async (lot_id: IBid['lot_id']) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios({
|
||||||
|
url: `bid-histories/detail/${lot_id}`,
|
||||||
|
withCredentials: true,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -32,14 +32,14 @@ export const createBid = async (bid: Omit<IBid, 'id' | 'created_at' | 'updated_a
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBid = async (bid: Partial<IBid>) => {
|
export const updateBid = async (bid: Partial<IBid>) => {
|
||||||
const { step_price, max_price, quantity } = removeFalsyValues(bid);
|
const { plus_price, max_price, quantity } = removeFalsyValues(bid, ['plus_price']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: 'bids/' + bid.id,
|
url: 'bids/' + bid.id,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: { step_price, max_price, quantity },
|
data: { plus_price, max_price, quantity },
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSuccess(data);
|
handleSuccess(data);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface IBidModelProps extends ModalProps {
|
||||||
const schema = {
|
const schema = {
|
||||||
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
||||||
max_price: z.number({ message: 'Max price is required' }).min(1, 'Max price must be at least 1'),
|
max_price: z.number({ message: 'Max price is required' }).min(1, 'Max price must be at least 1'),
|
||||||
step_price: z.number().min(0, 'Step price must be at least 1').optional(),
|
plus_price: z.number().min(0, 'Plus price must be at least 1').optional(),
|
||||||
quantity: z.number().min(1, 'Quantity must be at least 1').optional(),
|
quantity: z.number().min(1, 'Quantity must be at least 1').optional(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,9 +50,9 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const { url, max_price, step_price } = values;
|
const { url, max_price, plus_price } = values;
|
||||||
|
|
||||||
const result = await createBid({ url, max_price, step_price } as IBid);
|
const result = await createBid({ url, max_price, plus_price } as IBid);
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
|
|
@ -92,9 +92,10 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||||
|
{data && data.name && <TextInput className="col-span-2" readOnly={!!data} size="sm" label="Name" value={data.name} />}
|
||||||
<TextInput className="col-span-2" readOnly={!!data} size="sm" label="Url" {...form.getInputProps('url')} />
|
<TextInput className="col-span-2" readOnly={!!data} size="sm" label="Url" {...form.getInputProps('url')} />
|
||||||
<NumberInput className="col-span-2" size="sm" label="Max price" {...form.getInputProps('max_price')} />
|
<NumberInput className="col-span-2" size="sm" label="Max price" {...form.getInputProps('max_price')} />
|
||||||
<NumberInput size="sm" label="Step price" {...form.getInputProps('step_price')} />
|
<NumberInput size="sm" label="Plus price" {...form.getInputProps('plus_price')} />
|
||||||
<NumberInput size="sm" label="Quantity" {...form.getInputProps('quantity')} />
|
<NumberInput size="sm" label="Quantity" {...form.getInputProps('quantity')} />
|
||||||
|
|
||||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as ShowHistoriesModal } from './show-histories-modal';
|
export { default as ShowHistoriesModal } from './show-histories-modal';
|
||||||
|
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
|
||||||
export { default as BidModal } from './bid-modal';
|
export { default as BidModal } from './bid-modal';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { Modal, ModalProps, Table } from '@mantine/core';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { extractNumber } from '../../lib/table/ultils';
|
||||||
|
import { IBid } from '../../system/type';
|
||||||
|
import { formatTime } from '../../utils';
|
||||||
|
import { getDetailBidHistories } from '../../apis/bid-histories';
|
||||||
|
|
||||||
|
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
|
||||||
|
data: IBid | null;
|
||||||
|
onUpdated?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
|
||||||
|
const [histories, setHistories] = useState<Record<string, string>[]>([]);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
return histories.map((element) => (
|
||||||
|
<Table.Tr key={element.LotId}>
|
||||||
|
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
|
||||||
|
<Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||||
|
<Table.Td>{`AU $${element['Price']}`}</Table.Td>
|
||||||
|
<Table.Td>{`${element['Quantity']}`}</Table.Td>
|
||||||
|
<Table.Td>{`${element['WinningQuantity']}`}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
}, [histories]);
|
||||||
|
|
||||||
|
const handleCallApi = useCallback(async () => {
|
||||||
|
if (!data?.lot_id) {
|
||||||
|
setHistories([]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getDetailBidHistories(data?.lot_id);
|
||||||
|
|
||||||
|
if (response.data && response.data) {
|
||||||
|
setHistories(response.data);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleCallApi();
|
||||||
|
}, [handleCallApi]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
|
||||||
|
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Bidding Details</Table.Th>
|
||||||
|
<Table.Th>Bid Time</Table.Th>
|
||||||
|
<Table.Th>Bid Price</Table.Th>
|
||||||
|
<Table.Th>Bid Qty</Table.Th>
|
||||||
|
<Table.Th>Win Qty</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{histories.length <= 0 ? (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={5} className="text-center">
|
||||||
|
None
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
) : (
|
||||||
|
rows
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ export interface IShowHistoriesModalModalProps extends ModalProps {
|
||||||
onUpdated?: () => void;
|
onUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfigModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
||||||
const rows = data?.histories.map((element) => (
|
const rows = data?.histories.map((element) => (
|
||||||
<Table.Tr key={element.id}>
|
<Table.Tr key={element.id}>
|
||||||
<Table.Td>{element.id}</Table.Td>
|
<Table.Td>{element.id}</Table.Td>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import { Box, Checkbox, MantineStyleProp, Table as MTable, TableProps as MTableP
|
||||||
import { AxiosError, AxiosResponse } from 'axios';
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import React, { ChangeEvent, CSSProperties, ReactNode, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
import React, { ChangeEvent, CSSProperties, ReactNode, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import TableActions, { ITableActionsProps } from './action';
|
import TableActions, { ITableActionsProps } from './action';
|
||||||
import Filter from './filter';
|
import Filter from './filter';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { IChooseOptions, IColumn, IColumnStyle, IDataFilter, IFilterItemProps, IOptions, ITableFilter, ITableShort, TableChildProps, TKeyPagination, TRefTableFn } from './type';
|
import { IChooseOptions, IColumn, IColumnStyle, IDataFilter, IFilterItemProps, IOptions, ITableFilter, ITableShort, TableChildProps, TKeyPagination, TRefTableFn } from './type';
|
||||||
import { defaultPathToData, defaultPrefixShort, defaultStyleHightlight, flowShort, getParamsData as getParamsFromURL, searchKey } from './ultils';
|
import { defaultPathToData, defaultPrefixShort, defaultStyleHightlight, flowShort, getParamsData as getParamsFromURL, searchKey } from './ultils';
|
||||||
|
|
||||||
|
|
@ -639,6 +639,7 @@ const Table = <R extends Record<string, any>>({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MTable.ScrollContainer minWidth={500} type="scrollarea">
|
||||||
<MTable {...props}>
|
<MTable {...props}>
|
||||||
<MTable.Thead {...thead}>
|
<MTable.Thead {...thead}>
|
||||||
<MTable.Tr {...trhead}>
|
<MTable.Tr {...trhead}>
|
||||||
|
|
@ -769,6 +770,7 @@ const Table = <R extends Record<string, any>>({
|
||||||
))}
|
))}
|
||||||
</MTable.Tbody>
|
</MTable.Tbody>
|
||||||
</MTable>
|
</MTable>
|
||||||
|
</MTable.ScrollContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,8 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
|
||||||
return prev;
|
return prev;
|
||||||
}, {} as { [key: string]: string | number });
|
}, {} as { [key: string]: string | number });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function extractNumber(str: string) {
|
||||||
|
const match = str.match(/\d+(\.\d+)?/);
|
||||||
|
return match ? parseFloat(match[0]) : null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { IconAd, IconAdOff, IconEdit, IconHistory, IconMenu, IconTrash } from '@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
|
||||||
import { BidModal, ShowHistoriesModal } from '../components/bid';
|
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
|
||||||
import Table from '../lib/table/table';
|
import Table from '../lib/table/table';
|
||||||
import { IColumn, TRefTableFn } from '../lib/table/type';
|
import { IColumn, TRefTableFn } from '../lib/table/type';
|
||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||||
|
|
@ -20,13 +20,14 @@ export default function Bids() {
|
||||||
const { setConfirm } = useConfirmStore();
|
const { setConfirm } = useConfirmStore();
|
||||||
|
|
||||||
const [openedHistories, historiesModel] = useDisclosure(false);
|
const [openedHistories, historiesModel] = useDisclosure(false);
|
||||||
|
const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
|
||||||
const [openedBid, bidModal] = useDisclosure(false);
|
const [openedBid, bidModal] = useDisclosure(false);
|
||||||
|
|
||||||
const columns: IColumn<IBid>[] = [
|
const columns: IColumn<IBid>[] = [
|
||||||
{
|
{
|
||||||
key: 'id',
|
key: 'name',
|
||||||
title: 'ID',
|
title: 'Name',
|
||||||
typeFilter: 'number',
|
typeFilter: 'text',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'lot_id',
|
key: 'lot_id',
|
||||||
|
|
@ -38,14 +39,14 @@ export default function Bids() {
|
||||||
title: 'Model',
|
title: 'Model',
|
||||||
typeFilter: 'text',
|
typeFilter: 'text',
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// key: 'quantity',
|
||||||
|
// title: 'Qty',
|
||||||
|
// typeFilter: 'number',
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
key: 'quantity',
|
key: 'plus_price',
|
||||||
title: 'Qty',
|
title: 'Plus price',
|
||||||
typeFilter: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'step_price',
|
|
||||||
title: 'Step price',
|
|
||||||
typeFilter: 'number',
|
typeFilter: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -58,6 +59,11 @@ export default function Bids() {
|
||||||
title: 'Current price',
|
title: 'Current price',
|
||||||
typeFilter: 'number',
|
typeFilter: 'number',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'reserve_price',
|
||||||
|
title: 'Reserve price',
|
||||||
|
typeFilter: 'number',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'histories',
|
key: 'histories',
|
||||||
title: 'Current bid',
|
title: 'Current bid',
|
||||||
|
|
@ -107,14 +113,14 @@ export default function Bids() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
// {
|
||||||
key: 'updated_at',
|
// key: 'updated_at',
|
||||||
title: 'Update at',
|
// title: 'Update at',
|
||||||
typeFilter: 'none',
|
// typeFilter: 'none',
|
||||||
renderRow(row) {
|
// renderRow(row) {
|
||||||
return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
// return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleDelete = (bid: IBid) => {
|
const handleDelete = (bid: IBid) => {
|
||||||
|
|
@ -242,6 +248,15 @@ export default function Bids() {
|
||||||
>
|
>
|
||||||
Histories
|
Histories
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
setClickData(row);
|
||||||
|
historiesGraysApiModel.open();
|
||||||
|
}}
|
||||||
|
leftSection={<IconHistory size={14} />}
|
||||||
|
>
|
||||||
|
Bids
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
disabled={row.status === 'win-bid'}
|
disabled={row.status === 'win-bid'}
|
||||||
|
|
@ -293,6 +308,23 @@ export default function Bids() {
|
||||||
}}
|
}}
|
||||||
data={clickData}
|
data={clickData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ShowHistoriesBidGraysApiModal
|
||||||
|
onUpdated={() => {
|
||||||
|
if (refTableFn.current?.fetchData) {
|
||||||
|
refTableFn.current.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
opened={openedHistoriesGraysApi}
|
||||||
|
onClose={() => {
|
||||||
|
historiesGraysApiModel.close();
|
||||||
|
|
||||||
|
setClickData(null);
|
||||||
|
}}
|
||||||
|
data={clickData}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ import io from 'socket.io-client';
|
||||||
import { WorkingPage } from '../components/dashboard';
|
import { WorkingPage } from '../components/dashboard';
|
||||||
import { IBid, IWebBid } from '../system/type';
|
import { IBid, IWebBid } from '../system/type';
|
||||||
|
|
||||||
// Dùng singleton để giữ kết nối WebSocket khi chuyển trang
|
|
||||||
const socket = io(import.meta.env.VITE_SOCKET_URL, {
|
const socket = io(import.meta.env.VITE_SOCKET_URL, {
|
||||||
autoConnect: true, // Tránh tự động kết nối khi import file
|
autoConnect: true,
|
||||||
transports: ['websocket'], // Chỉ dùng WebSocket để giảm độ trễ
|
transports: ['websocket'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function DashBoard() {
|
export default function DashBoard() {
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,14 @@ export interface ITimestamp {
|
||||||
export interface IBid extends ITimestamp {
|
export interface IBid extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
max_price: number;
|
max_price: number;
|
||||||
|
reserve_price: number;
|
||||||
current_price: number;
|
current_price: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
url: string;
|
url: string;
|
||||||
model: string;
|
model: string;
|
||||||
lot_id: string;
|
lot_id: string;
|
||||||
step_price: number;
|
plus_price: number;
|
||||||
close_time: string | null;
|
close_time: string | null;
|
||||||
start_bid_time: string | null;
|
start_bid_time: string | null;
|
||||||
first_bid: boolean;
|
first_bid: boolean;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.13.0",
|
"mysql2": "^3.13.0",
|
||||||
"nestjs-paginate": "^11.1.0",
|
"nestjs-paginate": "^11.1.0",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.13.0",
|
"mysql2": "^3.13.0",
|
||||||
"nestjs-paginate": "^11.1.0",
|
"nestjs-paginate": "^11.1.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { escapeMarkdownV2 } from 'src/ultils';
|
||||||
|
import { Bid } from '../entities/bid.entity';
|
||||||
|
import * as dayjs from 'dayjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BotTelegramApi {
|
||||||
|
private readonly botToken: string;
|
||||||
|
private readonly apiUrl: string;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.botToken = this.configService.get<string>('TELEGRAM_BOT_TOKEN');
|
||||||
|
this.apiUrl = `https://api.telegram.org/bot${this.botToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBidMessage(bid: Bid) {
|
||||||
|
return `<b>📢 New Bid Alert!</b>\n
|
||||||
|
<b>📌 Product:</b> ${bid.name ? `<b>${bid.name}</b>` : '_No name_'}\n
|
||||||
|
<b>💰 Current Price:</b> <code>${bid.current_price.toLocaleString()}${bid.plus_price ? ` (+${bid.plus_price.toLocaleString()})` : ''}</code>\n
|
||||||
|
<b>🏆 Max Price:</b> <code>${bid.max_price.toLocaleString()}</code>\n
|
||||||
|
<b>🔖 Starting Price:</b> <code>${bid.reserve_price.toLocaleString()}</code>\n\n
|
||||||
|
<b>📦 Model:</b> <code>${bid.model || 'None'}</code>\n
|
||||||
|
<b>🔢 Quantity:</b> <code>${bid.quantity}</code>\n
|
||||||
|
<b>🆔 Lot ID:</b> <code>${bid.lot_id ? bid.lot_id.toString() : 'None'}</code>\n\n
|
||||||
|
<b>⏳ Closing Time:</b> ${bid.close_time ? `<code>${dayjs(bid.close_time).format('HH:mm:ss DD/MM/YYYY')}</code>` : '_Not available_'}\n
|
||||||
|
<b>🚀 Start Time:</b> ${bid.start_bid_time ? `<code>${dayjs(bid.start_bid_time).format('HH:mm:ss DD/MM/YYYY')}</code>` : '_Not available_'}\n\n
|
||||||
|
<b>📈 Bid History:</b>\n${
|
||||||
|
bid.histories.length > 0
|
||||||
|
? bid.histories
|
||||||
|
.map(
|
||||||
|
(h, index) =>
|
||||||
|
` ${index + 1}. 💵 <code>${index === 0 ? '🔴' : ''}${h.price.toLocaleString()}</code> 🕒 <code>${dayjs(h.updated_at).format('HH:mm:ss DD/MM/YYYY')}</code>`,
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
: '_No bid history yet_'
|
||||||
|
}\n\n
|
||||||
|
<b>📝 Status:</b> <code>${bid.status.replace('-', ' ')}</code>\n\n
|
||||||
|
⬇️ <b>View Details:</b> <a href="${bid.url}">Click here</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(
|
||||||
|
text: string,
|
||||||
|
bonusBody: Record<string, any> = {},
|
||||||
|
chatId: string = this.configService.get<string>('CHAT_ID'),
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const url = `${this.apiUrl}/sendMessage`;
|
||||||
|
const response = await axios.post(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
text: text,
|
||||||
|
...bonusBody,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
family: 4,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendBidInfo(bid: Bid): Promise<void> {
|
||||||
|
const text = this.formatBidMessage(bid);
|
||||||
|
|
||||||
|
console.log(text);
|
||||||
|
await this.sendMessage(text, {
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import axios from 'axios';
|
||||||
|
import AppResponse from 'src/response/app-response';
|
||||||
|
import { Bid } from '../entities/bid.entity';
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return AppResponse.toResponse([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,8 @@ import { BidHistoriesService } from './services/bid-histories.service';
|
||||||
import { BidsService } from './services/bids.service';
|
import { BidsService } from './services/bids.service';
|
||||||
import { OutBidLogsService } from './services/out-bid-logs.service';
|
import { OutBidLogsService } from './services/out-bid-logs.service';
|
||||||
import { WebBidsService } from './services/web-bids.service';
|
import { WebBidsService } from './services/web-bids.service';
|
||||||
|
import { BotTelegramApi } from './apis/bot-telegram.api';
|
||||||
|
import { GraysApi } from './apis/grays.api';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -40,6 +42,8 @@ import { WebBidsService } from './services/web-bids.service';
|
||||||
BidGateway,
|
BidGateway,
|
||||||
OutBidLogsService,
|
OutBidLogsService,
|
||||||
WebBidsService,
|
WebBidsService,
|
||||||
|
BotTelegramApi,
|
||||||
|
GraysApi,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BidsModule {}
|
export class BidsModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||||
import { BidsService } from '../../services/bids.service';
|
import { BidsService } from '../../services/bids.service';
|
||||||
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
|
import { CreateBidDto } from '../../dto/bid/create-bid.dto';
|
||||||
import { BidHistoriesService } from '../../services/bid-histories.service';
|
import { BidHistoriesService } from '../../services/bid-histories.service';
|
||||||
import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
|
import { CreateBidHistoryDto } from '../../dto/bid-history/create-bid-history.dto';
|
||||||
|
import { Bid } from '../../entities/bid.entity';
|
||||||
|
import { GraysApi } from '../../apis/grays.api';
|
||||||
|
|
||||||
@Controller('admin/bid-histories')
|
@Controller('admin/bid-histories')
|
||||||
export class AdminBidHistoriesController {
|
export class AdminBidHistoriesController {
|
||||||
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
constructor(
|
||||||
|
private readonly bidHistoriesService: BidHistoriesService,
|
||||||
|
private readonly graysApi: GraysApi,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() data: CreateBidHistoryDto) {
|
create(@Body() data: CreateBidHistoryDto) {
|
||||||
return this.bidHistoriesService.create(data);
|
return this.bidHistoriesService.create(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('detail/:lot_id')
|
||||||
|
async getBidHistories(@Param('lot_id') lot_id: Bid['lot_id']) {
|
||||||
|
return await this.graysApi.getHistoriesBid(lot_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export class BidHistoriesController {
|
||||||
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() data: CreateBidHistoryDto) {
|
async create(@Body() data: CreateBidHistoryDto) {
|
||||||
return this.bidHistoriesService.create(data);
|
return await this.bidHistoriesService.create(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,8 @@ export class ClientUpdateBidDto {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
current_price: number;
|
current_price: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
reserve_price: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,5 @@ export class CreateBidDto {
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
step_price: number;
|
plus_price: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,5 @@ export class UpdateBidDto {
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
step_price: number;
|
plus_price: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ export class Bid extends Timestamp {
|
||||||
lot_id: string;
|
lot_id: string;
|
||||||
|
|
||||||
@Column({ default: 0 })
|
@Column({ default: 0 })
|
||||||
step_price: number;
|
plus_price: number;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
reserve_price: number;
|
||||||
|
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
import { Timestamp } from './timestamp';
|
import { Timestamp } from './timestamp';
|
||||||
|
|
||||||
@Entity('out-bid-logs')
|
@Entity('out-bid-logs')
|
||||||
|
@Unique(['model', 'out_price'])
|
||||||
export class OutBidLog extends Timestamp {
|
export class OutBidLog extends Timestamp {
|
||||||
@PrimaryGeneratedColumn('increment')
|
@PrimaryGeneratedColumn('increment')
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@Column({ unique: true, default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
model: string | null;
|
model: string | null;
|
||||||
|
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,11 @@ export class BidGateway implements OnGatewayConnection {
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.eventEmitter.on('bids.updated', (data) => {
|
this.eventEmitter.on('bids.updated', (data) => {
|
||||||
this.server.emit('bidsUpdated', data); // Gửi sự kiện WebSocket đến client
|
this.server.emit('bidsUpdated', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.on('web.updated', (data) => {
|
||||||
|
this.server.emit('webUpdated', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.on('working', (data) => {
|
this.eventEmitter.on('working', (data) => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Repository } from 'typeorm';
|
||||||
import { BidHistory } from '../entities/bid-history.entity';
|
import { BidHistory } from '../entities/bid-history.entity';
|
||||||
import { Bid } from '../entities/bid.entity';
|
import { Bid } from '../entities/bid.entity';
|
||||||
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
|
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
|
||||||
|
import { BotTelegramApi } from '../apis/bot-telegram.api';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BidHistoriesService {
|
export class BidHistoriesService {
|
||||||
|
|
@ -19,6 +20,7 @@ export class BidHistoriesService {
|
||||||
readonly bidHistoriesRepo: Repository<BidHistory>,
|
readonly bidHistoriesRepo: Repository<BidHistory>,
|
||||||
@InjectRepository(Bid)
|
@InjectRepository(Bid)
|
||||||
readonly bidsRepo: Repository<Bid>,
|
readonly bidsRepo: Repository<Bid>,
|
||||||
|
private readonly botTelegramApi: BotTelegramApi,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async index() {
|
async index() {
|
||||||
|
|
@ -47,7 +49,7 @@ export class BidHistoriesService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (price + bid.step_price > bid.max_price) {
|
if (price + bid.plus_price > bid.max_price) {
|
||||||
this.bidsRepo.update(bid_id, { status: 'out-bid' });
|
this.bidsRepo.update(bid_id, { status: 'out-bid' });
|
||||||
|
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
|
|
@ -70,6 +72,8 @@ export class BidHistoriesService {
|
||||||
this.bidsRepo.update(bid_id, { first_bid: false });
|
this.bidsRepo.update(bid_id, { first_bid: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.botTelegramApi.sendBidInfo({ ...bid, histories: response });
|
||||||
|
|
||||||
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,10 @@ export class BidsService {
|
||||||
|
|
||||||
const result = await this.bidsRepo.update(id, {
|
const result = await this.bidsRepo.update(id, {
|
||||||
...data,
|
...data,
|
||||||
status:
|
// status:
|
||||||
prev.max_price + prev.step_price > data.max_price
|
// prev.max_price + prev.plus_price > data.max_price
|
||||||
? 'out-bid'
|
// ? 'out-bid'
|
||||||
: prev.status,
|
// : prev.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) throw new BadRequestException(false);
|
if (!result) throw new BadRequestException(false);
|
||||||
|
|
@ -167,7 +167,7 @@ export class BidsService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastHistory && lastHistory.price + bid.step_price >= bid.max_price) {
|
if (lastHistory && lastHistory.price + bid.plus_price >= bid.max_price) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
|
AppResponse.toResponse(false, { message: 'Price is out of Max Price' }),
|
||||||
);
|
);
|
||||||
|
|
@ -175,7 +175,7 @@ export class BidsService {
|
||||||
|
|
||||||
if (bid.close_time && isTimeReached(bid.close_time)) {
|
if (bid.close_time && isTimeReached(bid.close_time)) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
AppResponse.toResponse(false, { message: 'Close was closed' }),
|
AppResponse.toResponse(false, { message: 'Product was closed' }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +201,7 @@ export class BidsService {
|
||||||
bid.start_bid_time = subtractMinutes(data.close_time, 5);
|
bid.start_bid_time = subtractMinutes(data.close_time, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.current_price > bid.max_price + bid.step_price) {
|
if (data.current_price > bid.max_price + bid.plus_price) {
|
||||||
bid.status = 'out-bid';
|
bid.status = 'out-bid';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,17 @@ export class OutBidLogsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateOutBidLogDto[]) {
|
async create(data: CreateOutBidLogDto[]) {
|
||||||
|
try {
|
||||||
const result = await this.outbidLogRepo.upsert(data, {
|
const result = await this.outbidLogRepo.upsert(data, {
|
||||||
conflictPaths: ['model', 'id'],
|
conflictPaths: ['model', 'lot_id'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) throw new BadRequestException(false);
|
if (!result) throw new BadRequestException(false);
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,12 @@ export class WebBidsService {
|
||||||
this.eventEmitter.emit('bids.updated', data);
|
this.eventEmitter.emit('bids.updated', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async emitAccountUpdate(id: WebBid['id']) {
|
||||||
|
const data = await this.webBidRepo.findOne({ where: { id } });
|
||||||
|
|
||||||
|
this.eventEmitter.emit('web.updated', data || null);
|
||||||
|
}
|
||||||
|
|
||||||
async createByUrl(url: string) {
|
async createByUrl(url: string) {
|
||||||
const originUrl = extractDomain(url);
|
const originUrl = extractDomain(url);
|
||||||
|
|
||||||
|
|
@ -131,6 +137,9 @@ export class WebBidsService {
|
||||||
|
|
||||||
if (!result) throw new BadRequestException(false);
|
if (!result) throw new BadRequestException(false);
|
||||||
|
|
||||||
|
if (data.password !== prev.password || data.username !== prev.username) {
|
||||||
|
this.emitAccountUpdate(prev.id);
|
||||||
|
}
|
||||||
this.emitAllBidEvent();
|
this.emitAllBidEvent();
|
||||||
|
|
||||||
return AppResponse.toResponse(true);
|
return AppResponse.toResponse(true);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
database: configService.get<string>('DB_NAME'),
|
database: configService.get<string>('DB_NAME'),
|
||||||
charset: 'utf8mb4_unicode_ci',
|
charset: 'utf8mb4_unicode_ci',
|
||||||
entities: ['dist/**/*.entity{.ts,.js}'],
|
entities: ['dist/**/*.entity{.ts,.js}'],
|
||||||
synchronize: true,
|
synchronize:
|
||||||
|
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,7 @@ export function extractDomain(url: string): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function escapeMarkdownV2(text: string) {
|
||||||
|
return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import 'dotenv/config';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { io } from 'socket.io-client';
|
import { io } from 'socket.io-client';
|
||||||
import { GrayApiBid } from './models/grays.com/grays-api-bid.js';
|
import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js';
|
||||||
import { GraysProductBid } from './models/grays.com/grays-product-bid.js';
|
import browser from './system/browser.js';
|
||||||
import configs from './system/config.js';
|
import configs from './system/config.js';
|
||||||
import { isTimeReached, safeClosePage } from './system/utils.js';
|
import { isTimeReached, safeClosePage } from './system/utils.js';
|
||||||
import browser from './system/browser.js';
|
|
||||||
|
|
||||||
let MANAGER_BIDS = [];
|
let MANAGER_BIDS = [];
|
||||||
|
|
||||||
|
|
@ -12,31 +12,7 @@ let _INTERVAL_TRACKING_ID = null;
|
||||||
let _CLEAR_LAZY_TAB_ID = null;
|
let _CLEAR_LAZY_TAB_ID = null;
|
||||||
let _WORK_TRACKING_ID = null;
|
let _WORK_TRACKING_ID = null;
|
||||||
|
|
||||||
const handleCloseRemoveProduct = (data) => {
|
global.IS_CLEANING = false;
|
||||||
if (!Array.isArray(data)) return;
|
|
||||||
|
|
||||||
data.forEach(async (item) => {
|
|
||||||
if (item.page_context) {
|
|
||||||
safeClosePage(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createBidProduct = (web, data) => {
|
|
||||||
switch (web.origin_url) {
|
|
||||||
case configs.WEB_URLS.GRAYS: {
|
|
||||||
return new GraysProductBid({ ...data });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createApiBid = (web) => {
|
|
||||||
switch (web.origin_url) {
|
|
||||||
case configs.WEB_URLS.GRAYS: {
|
|
||||||
return new GrayApiBid({ ...web });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateProductTabs = (data) => {
|
const handleUpdateProductTabs = (data) => {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
|
|
@ -75,6 +51,7 @@ const handleUpdateProductTabs = (data) => {
|
||||||
const tracking = async () => {
|
const tracking = async () => {
|
||||||
if (_INTERVAL_TRACKING_ID) {
|
if (_INTERVAL_TRACKING_ID) {
|
||||||
clearInterval(_INTERVAL_TRACKING_ID);
|
clearInterval(_INTERVAL_TRACKING_ID);
|
||||||
|
_INTERVAL_TRACKING_ID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_INTERVAL_TRACKING_ID = setInterval(async () => {
|
_INTERVAL_TRACKING_ID = setInterval(async () => {
|
||||||
|
|
@ -121,42 +98,129 @@ const tracking = async () => {
|
||||||
}, configs.AUTO_TRACKING_DELAY);
|
}, configs.AUTO_TRACKING_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const clearLazyTab = async () => {
|
||||||
|
// if (_CLEAR_LAZY_TAB_ID) {
|
||||||
|
// clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||||
|
// _CLEAR_LAZY_TAB_ID = null; // Reset tránh memory leak
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// _CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||||
|
// if (!browser) {
|
||||||
|
// console.warn('⚠️ Browser is not available.');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const pages = await browser.pages();
|
||||||
|
|
||||||
|
// // Lấy danh sách URL từ flattenedArray
|
||||||
|
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||||
|
// // item.url,
|
||||||
|
// ...item.children.map((child) => child.url),
|
||||||
|
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// '🔍 Page URLs:',
|
||||||
|
// pages.map((page) => page.url()),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// for (const page of pages) {
|
||||||
|
// const pageUrl = page.url();
|
||||||
|
|
||||||
|
// if (!activeUrls.includes(pageUrl)) {
|
||||||
|
// if (!page.isClosed()) {
|
||||||
|
// try {
|
||||||
|
// 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);
|
||||||
|
// }
|
||||||
|
// }, configs.AUTO_TRACKING_CLEANING);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log('CLEAR LAZY TAB: ', error.message);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
const clearLazyTab = async () => {
|
const clearLazyTab = async () => {
|
||||||
if (_CLEAR_LAZY_TAB_ID) {
|
if (_CLEAR_LAZY_TAB_ID) {
|
||||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||||
|
_CLEAR_LAZY_TAB_ID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
_CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
_CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||||
|
if (!global.IS_CLEANING) return;
|
||||||
|
|
||||||
|
if (!browser) {
|
||||||
|
console.warn('⚠️ Browser is not available or disconnected.');
|
||||||
|
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||||
|
_CLEAR_LAZY_TAB_ID = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const pages = await browser.pages();
|
const pages = await browser.pages();
|
||||||
|
|
||||||
// Lấy danh sách URL từ flattenedArray
|
// Lấy danh sách URL từ flattenedArray
|
||||||
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||||
|
|
||||||
console.log({ activeUrls });
|
console.log(
|
||||||
|
'🔍 Page URLs:',
|
||||||
|
pages.map((page) => page.url()),
|
||||||
|
);
|
||||||
|
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
const pageUrl = page.url();
|
const pageUrl = page.url();
|
||||||
|
|
||||||
|
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
|
||||||
|
if (!pageUrl || pageUrl === 'about:blank') continue;
|
||||||
|
|
||||||
if (!activeUrls.includes(pageUrl)) {
|
if (!activeUrls.includes(pageUrl)) {
|
||||||
console.log(`Closing unused tab: ${pageUrl}`);
|
if (!page.isClosed() && browser.isConnected()) {
|
||||||
|
try {
|
||||||
await page.close();
|
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);
|
||||||
|
}
|
||||||
}, configs.AUTO_TRACKING_CLEANING);
|
}, configs.AUTO_TRACKING_CLEANING);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('CLEAR LAZY TAB ERROR: ', error.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const workTracking = () => {
|
const workTracking = () => {
|
||||||
|
try {
|
||||||
if (_WORK_TRACKING_ID) {
|
if (_WORK_TRACKING_ID) {
|
||||||
clearInterval(_WORK_TRACKING_ID);
|
clearInterval(_WORK_TRACKING_ID);
|
||||||
|
_WORK_TRACKING_ID = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_WORK_TRACKING_ID = setInterval(() => {
|
_WORK_TRACKING_ID = setInterval(() => {
|
||||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
||||||
|
|
||||||
for (const item of activeData) {
|
for (const item of activeData) {
|
||||||
|
if (item.page_context && !item.page_context.isClosed()) {
|
||||||
item.handleTakeWorkSnapshot();
|
item.handleTakeWorkSnapshot();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Loi oi day');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -180,6 +244,26 @@ const workTracking = () => {
|
||||||
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
|
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('webUpdated', async (data) => {
|
||||||
|
console.log('📢 Account was updated:', data);
|
||||||
|
|
||||||
|
const isDeleted = deleteProfile(data);
|
||||||
|
|
||||||
|
if (isDeleted) {
|
||||||
|
console.log('✅ Profile deleted successfully!');
|
||||||
|
|
||||||
|
const tabs = MANAGER_BIDS.filter((item) => item.url === data.url || item?.web_bid.url === data.url);
|
||||||
|
|
||||||
|
if (tabs.length <= 0) return;
|
||||||
|
|
||||||
|
await Promise.all(tabs.map((tab) => safeClosePage(tab)));
|
||||||
|
|
||||||
|
await Promise.all(MANAGER_BIDS.map((apiBid) => apiBid.listen_events()));
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ No profile found to delete.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// AUTO TRACKING
|
// AUTO TRACKING
|
||||||
tracking();
|
tracking();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import BID_TYPE from '../system/bid-type.js';
|
import BID_TYPE from '../system/bid-type.js';
|
||||||
import browser from '../system/browser.js';
|
import browser from '../system/browser.js';
|
||||||
import CONSTANTS from '../system/constants.js';
|
import CONSTANTS from '../system/constants.js';
|
||||||
import { sanitizeFileName } from '../system/utils.js';
|
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||||
import { Account } from './account.js';
|
|
||||||
import { Bid } from './bid.js';
|
import { Bid } from './bid.js';
|
||||||
import * as fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export class ApiBid extends Bid {
|
export class ApiBid extends Bid {
|
||||||
id;
|
id;
|
||||||
|
|
@ -93,7 +92,7 @@ export class ApiBid extends Bid {
|
||||||
async restoreContext() {
|
async restoreContext() {
|
||||||
if (!this.browser_context || !this.page_context) return;
|
if (!this.browser_context || !this.page_context) return;
|
||||||
|
|
||||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(this.origin_url) + '.json');
|
const filePath = getPathProfile(this.origin_url);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) return;
|
if (!fs.existsSync(filePath)) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import path from 'path';
|
||||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
||||||
import configs from '../../system/config.js';
|
import configs from '../../system/config.js';
|
||||||
import CONSTANTS from '../../system/constants.js';
|
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
|
||||||
import { delay, extractNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
|
||||||
import { ApiBid } from '../api-bid.js';
|
import { ApiBid } from '../api-bid.js';
|
||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
export class GrayApiBid extends ApiBid {
|
export class GrayApiBid extends ApiBid {
|
||||||
retry_login = 0;
|
retry_login = 0;
|
||||||
|
|
@ -129,20 +129,25 @@ export class GrayApiBid extends ApiBid {
|
||||||
async handleLogin() {
|
async handleLogin() {
|
||||||
const page = this.page_context;
|
const page = this.page_context;
|
||||||
|
|
||||||
|
const filePath = getPathProfile(this.origin_url);
|
||||||
|
|
||||||
// 🔍 Check if already logged in (login input should not be visible)
|
// 🔍 Check if already logged in (login input should not be visible)
|
||||||
if (!(await page.$('input[name="username"]'))) {
|
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
|
||||||
console.log('✅ Already logged in, skipping login.');
|
console.log('✅ Already logged in, skipping login.');
|
||||||
|
global.IS_CLEANING = true;
|
||||||
|
|
||||||
this.retry_login = 0; // Reset retry count
|
this.retry_login = 0; // Reset retry count
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔑 Starting login process...');
|
console.log('🔑 Starting login process...');
|
||||||
|
global.IS_CLEANING = false;
|
||||||
|
|
||||||
|
try {
|
||||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||||
await page.click('#loginButton');
|
await page.click('#loginButton');
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
|
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
|
||||||
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
|
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
|
||||||
|
|
@ -151,6 +156,7 @@ export class GrayApiBid extends ApiBid {
|
||||||
if (!(await page.$('input[name="username"]'))) {
|
if (!(await page.$('input[name="username"]'))) {
|
||||||
console.log('✅ Login successful!');
|
console.log('✅ Login successful!');
|
||||||
this.retry_login = 0; // Reset retry count after success
|
this.retry_login = 0; // Reset retry count after success
|
||||||
|
global.IS_CLEANING = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class GraysProductBid extends ProductBid {
|
||||||
return { result: false, bid_price: 0 };
|
return { result: false, bid_price: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bid_price = this.step_price + Number(price_value);
|
const bid_price = this.plus_price + Number(price_value);
|
||||||
|
|
||||||
if (bid_price > this.max_price) {
|
if (bid_price > this.max_price) {
|
||||||
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
|
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
|
||||||
|
|
@ -141,10 +141,10 @@ export class GraysProductBid extends ProductBid {
|
||||||
await delay(1000);
|
await delay(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUpdateBid({ lot_id, close_time, name, current_price }) {
|
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
|
||||||
if (close_time && this.close_time == close_time) return;
|
if (close_time && this.close_time == close_time) return;
|
||||||
|
|
||||||
const response = await updateBid(this.id, { lot_id, close_time, name, current_price });
|
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
this.lot_id = response.lot_id;
|
this.lot_id = response.lot_id;
|
||||||
|
|
@ -181,9 +181,9 @@ export class GraysProductBid extends ProductBid {
|
||||||
const current_price =
|
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)) || null;
|
(await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null;
|
||||||
|
|
||||||
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}`);
|
console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`);
|
||||||
|
|
||||||
this.handleUpdateBid({ lot_id, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
|
this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
|
||||||
|
|
||||||
const { result, bid_price } = await this.validate({ page, price_value });
|
const { result, bid_price } = await this.validate({ page, price_value });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import path from 'path';
|
||||||
import BID_TYPE from '../system/bid-type.js';
|
import BID_TYPE from '../system/bid-type.js';
|
||||||
import browser from '../system/browser.js';
|
import browser from '../system/browser.js';
|
||||||
import CONSTANTS from '../system/constants.js';
|
import CONSTANTS from '../system/constants.js';
|
||||||
import { delay, sanitizeFileName } from '../system/utils.js';
|
import { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||||
import { Bid } from './bid.js';
|
import { Bid } from './bid.js';
|
||||||
|
|
||||||
export class ProductBid extends Bid {
|
export class ProductBid extends Bid {
|
||||||
|
|
@ -11,7 +11,7 @@ export class ProductBid extends Bid {
|
||||||
max_price;
|
max_price;
|
||||||
model;
|
model;
|
||||||
lot_id;
|
lot_id;
|
||||||
step_price;
|
plus_price;
|
||||||
close_time;
|
close_time;
|
||||||
first_bid;
|
first_bid;
|
||||||
quantity;
|
quantity;
|
||||||
|
|
@ -23,11 +23,12 @@ export class ProductBid extends Bid {
|
||||||
web_bid;
|
web_bid;
|
||||||
current_price;
|
current_price;
|
||||||
name;
|
name;
|
||||||
|
reserve_price;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
url,
|
url,
|
||||||
max_price,
|
max_price,
|
||||||
step_price,
|
plus_price,
|
||||||
model,
|
model,
|
||||||
first_bid = false,
|
first_bid = false,
|
||||||
id,
|
id,
|
||||||
|
|
@ -40,12 +41,13 @@ export class ProductBid extends Bid {
|
||||||
start_bid_time,
|
start_bid_time,
|
||||||
web_bid,
|
web_bid,
|
||||||
current_price,
|
current_price,
|
||||||
|
reserve_price,
|
||||||
name,
|
name,
|
||||||
}) {
|
}) {
|
||||||
super(BID_TYPE.PRODUCT_TAB, url);
|
super(BID_TYPE.PRODUCT_TAB, url);
|
||||||
this.max_price = max_price || 0;
|
this.max_price = max_price || 0;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.step_price = step_price || 0;
|
this.plus_price = plus_price || 0;
|
||||||
this.first_bid = first_bid;
|
this.first_bid = first_bid;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.created_at = created_at;
|
this.created_at = created_at;
|
||||||
|
|
@ -58,12 +60,13 @@ export class ProductBid extends Bid {
|
||||||
this.web_bid = web_bid;
|
this.web_bid = web_bid;
|
||||||
this.current_price = current_price;
|
this.current_price = current_price;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.reserve_price = reserve_price;
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewData({
|
setNewData({
|
||||||
url,
|
url,
|
||||||
max_price,
|
max_price,
|
||||||
step_price,
|
plus_price,
|
||||||
model,
|
model,
|
||||||
first_bid = false,
|
first_bid = false,
|
||||||
id,
|
id,
|
||||||
|
|
@ -76,11 +79,12 @@ export class ProductBid extends Bid {
|
||||||
start_bid_time,
|
start_bid_time,
|
||||||
web_bid,
|
web_bid,
|
||||||
current_price,
|
current_price,
|
||||||
|
reserve_price,
|
||||||
name,
|
name,
|
||||||
}) {
|
}) {
|
||||||
this.max_price = max_price || 0;
|
this.max_price = max_price || 0;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.step_price = step_price || 0;
|
this.plus_price = plus_price || 0;
|
||||||
this.first_bid = first_bid;
|
this.first_bid = first_bid;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.created_at = created_at;
|
this.created_at = created_at;
|
||||||
|
|
@ -94,6 +98,7 @@ export class ProductBid extends Bid {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.current_price = current_price;
|
this.current_price = current_price;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.reserve_price = reserve_price;
|
||||||
}
|
}
|
||||||
|
|
||||||
puppeteer_connect = async () => {
|
puppeteer_connect = async () => {
|
||||||
|
|
@ -107,7 +112,7 @@ export class ProductBid extends Bid {
|
||||||
const statusInit = await this.restoreContext(context);
|
const statusInit = await this.restoreContext(context);
|
||||||
|
|
||||||
if (!statusInit) {
|
if (!statusInit) {
|
||||||
console.log(`⚠️ Restore failed. Restart count`);
|
console.log(`⚠️ Restore failed.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +122,7 @@ export class ProductBid extends Bid {
|
||||||
};
|
};
|
||||||
|
|
||||||
async restoreContext(context) {
|
async restoreContext(context) {
|
||||||
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(this.web_bid.origin_url) + '.json');
|
const filePath = getPathProfile(this.web_bid.origin_url);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) return false;
|
if (!fs.existsSync(filePath)) return false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"puppeteer": "^24.4.0",
|
"puppeteer": "^24.4.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
|
@ -480,6 +481,18 @@
|
||||||
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
|
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"puppeteer": "^24.4.0",
|
"puppeteer": "^24.4.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
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 configs from '../system/config.js';
|
||||||
|
import CONSTANTS from '../system/constants.js';
|
||||||
|
import { sanitizeFileName } from '../system/utils.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
export const handleCloseRemoveProduct = (data) => {
|
||||||
|
if (!Array.isArray(data)) return;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createApiBid = (web) => {
|
||||||
|
switch (web.origin_url) {
|
||||||
|
case configs.WEB_URLS.GRAYS: {
|
||||||
|
return new GrayApiBid({ ...web });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteProfile = (data) => {
|
||||||
|
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(data.origin_url) + '.json');
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
@ -28,13 +28,13 @@ export const updateBid = async (id, values) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!data || !data?.data) {
|
if (!data || !data?.data) {
|
||||||
console.log('❌ UPDATE FAILURE');
|
console.log('❌ UPDATE FAILURE (UPDATE BID)');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('❌ ERROR IN SERVER: ', error);
|
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import ax from 'axios';
|
import ax from 'axios';
|
||||||
|
|
||||||
const axios = ax.create({
|
const axios = ax.create({
|
||||||
baseURL: 'http://localhost:4000/api/v1/',
|
// baseURL: 'http://172.18.2.125/api/v1/',
|
||||||
|
baseURL: process.env.BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default axios;
|
export default axios;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
|
|
||||||
puppeteer.use(StealthPlugin());
|
puppeteer.use(StealthPlugin());
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: false, // Để false để quan sát quá trình login
|
headless: process.env.ENVIRONMENT === 'prod' ? true : false,
|
||||||
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
const configs = {
|
const configs = {
|
||||||
AUTO_TRACKING_DELAY: 5000,
|
AUTO_TRACKING_DELAY: 5000,
|
||||||
AUTO_TRACKING_CLEANING: 10000,
|
AUTO_TRACKING_CLEANING: 10000,
|
||||||
SOCKET_URL: 'http://localhost:4000',
|
SOCKET_URL: process.env.SOCKET_URL,
|
||||||
WEB_URLS: {
|
WEB_URLS: {
|
||||||
GRAYS: `https://www.grays.com`,
|
GRAYS: `https://www.grays.com`,
|
||||||
},
|
},
|
||||||
WEB_CONFIGS: {
|
WEB_CONFIGS: {
|
||||||
GRAYS: {
|
GRAYS: {
|
||||||
AUTO_CALL_API_TO_TRACKING: 10000,
|
AUTO_CALL_API_TO_TRACKING: 3000,
|
||||||
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
|
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
||||||
console.log(`📂 Save at folder: ${typeDir}`);
|
console.log(`📂 Save at folder: ${typeDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.waitForSelector('body', { visible: true });
|
await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
||||||
|
|
||||||
// Chụp ảnh màn hình và lưu vào filePath
|
// Chụp ảnh màn hình và lưu vào filePath
|
||||||
await page.screenshot({ path: filePath, fullPage: true });
|
await page.screenshot({ path: filePath, fullPage: true });
|
||||||
|
|
@ -42,6 +42,7 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
||||||
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export async function safeClosePage(item) {
|
export async function safeClosePage(item) {
|
||||||
|
try {
|
||||||
const page = item.page_context;
|
const page = item.page_context;
|
||||||
|
|
||||||
if (!page?.isClosed() && page?.close) {
|
if (!page?.isClosed() && page?.close) {
|
||||||
|
|
@ -49,6 +50,12 @@ export async function safeClosePage(item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
item.page_context = undefined;
|
item.page_context = undefined;
|
||||||
|
if (item?.page_context) {
|
||||||
|
item.page_context = undefined;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Can't close item: " + item.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTimeReached(targetTime) {
|
export function isTimeReached(targetTime) {
|
||||||
|
|
@ -68,3 +75,7 @@ export function extractNumber(str) {
|
||||||
export const sanitizeFileName = (url) => {
|
export const sanitizeFileName = (url) => {
|
||||||
return url.replace(/[:\/]/g, '_');
|
return url.replace(/[:\/]/g, '_');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPathProfile = (origin_url) => {
|
||||||
|
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue