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>) => {
|
||||
const { step_price, max_price, quantity } = removeFalsyValues(bid);
|
||||
const { plus_price, max_price, quantity } = removeFalsyValues(bid, ['plus_price']);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: 'bids/' + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
data: { step_price, max_price, quantity },
|
||||
data: { plus_price, max_price, quantity },
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export interface IBidModelProps extends ModalProps {
|
|||
const schema = {
|
||||
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'),
|
||||
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(),
|
||||
};
|
||||
|
||||
|
|
@ -50,9 +50,9 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
|||
},
|
||||
});
|
||||
} 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;
|
||||
|
||||
|
|
@ -92,9 +92,10 @@ export default function BidModal({ data, onUpdated, ...props }: IBidModelProps)
|
|||
centered
|
||||
>
|
||||
<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')} />
|
||||
<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')} />
|
||||
|
||||
<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 ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-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;
|
||||
}
|
||||
|
||||
export default function ConfigModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
||||
export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowHistoriesModalModalProps) {
|
||||
const rows = data?.histories.map((element) => (
|
||||
<Table.Tr key={element.id}>
|
||||
<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 DOMPurify from 'dompurify';
|
||||
import React, { ChangeEvent, CSSProperties, ReactNode, useCallback, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import TableActions, { ITableActionsProps } from './action';
|
||||
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 { defaultPathToData, defaultPrefixShort, defaultStyleHightlight, flowShort, getParamsData as getParamsFromURL, searchKey } from './ultils';
|
||||
|
||||
|
|
@ -639,136 +639,138 @@ const Table = <R extends Record<string, any>>({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<MTable {...props}>
|
||||
<MTable.Thead {...thead}>
|
||||
<MTable.Tr {...trhead}>
|
||||
{showChooses && (
|
||||
<MTable.Th {...th}>
|
||||
{chooseOptions?.renderHead ? (
|
||||
chooseOptions.renderHead(chooses, handleChooses)
|
||||
) : (
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
input: 'cursor-pointer',
|
||||
}}
|
||||
checked={checkedAll}
|
||||
onChange={handleChooses}
|
||||
size="xs"
|
||||
/>
|
||||
<MTable.ScrollContainer minWidth={500} type="scrollarea">
|
||||
<MTable {...props}>
|
||||
<MTable.Thead {...thead}>
|
||||
<MTable.Tr {...trhead}>
|
||||
{showChooses && (
|
||||
<MTable.Th {...th}>
|
||||
{chooseOptions?.renderHead ? (
|
||||
chooseOptions.renderHead(chooses, handleChooses)
|
||||
) : (
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
input: 'cursor-pointer',
|
||||
}}
|
||||
checked={checkedAll}
|
||||
onChange={handleChooses}
|
||||
size="xs"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</MTable.Th>
|
||||
)}
|
||||
|
||||
{columns.map((column) => (
|
||||
<MTable.Th {...th} style={{ ...(column.style ? (renderStyleHead(column.style) as MantineStyleProp) : th?.style) }} key={`${column.key}-${uuid()}`}>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
...(styleDefaultHead || {}),
|
||||
...column.styleDefaultHead,
|
||||
}}
|
||||
>
|
||||
{column.renderColumn ? column.renderColumn(column) : column.title}
|
||||
|
||||
{renderShortIcon(column)}
|
||||
</Box>
|
||||
)}
|
||||
</MTable.Th>
|
||||
)}
|
||||
</MTable.Th>
|
||||
))}
|
||||
|
||||
{columns.map((column) => (
|
||||
<MTable.Th {...th} style={{ ...(column.style ? (renderStyleHead(column.style) as MantineStyleProp) : th?.style) }} key={`${column.key}-${uuid()}`}>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '6px',
|
||||
...(styleDefaultHead || {}),
|
||||
...column.styleDefaultHead,
|
||||
}}
|
||||
>
|
||||
{column.renderColumn ? column.renderColumn(column) : column.title}
|
||||
{actions && (
|
||||
<MTable.Th {...th} key={'__action_head_' + columns.length + 1}>
|
||||
{actions.title ? actions.title : 'Action'}
|
||||
</MTable.Th>
|
||||
)}
|
||||
</MTable.Tr>
|
||||
</MTable.Thead>
|
||||
<MTable.Tbody {...tbody} className="relative">
|
||||
{rowsData.length > 0 &&
|
||||
rowsData.map((row, index) => (
|
||||
<MTable.Tr {...trbody} key={row[rowKey]}>
|
||||
{showChooses && (
|
||||
<MTable.Td key={'__choose_' + index} {...td} {...chooseOptions?.defaultBodyProps}>
|
||||
{chooseOptions?.renderBody ? (
|
||||
chooseOptions.renderBody(chooses, row, (e) => handleChooseSingle(e, row))
|
||||
) : (
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
input: 'cursor-pointer',
|
||||
}}
|
||||
checked={chooses.some((choose) => JSON.stringify(choose) === JSON.stringify(row))}
|
||||
onChange={(e) => handleChooseSingle(e, row)}
|
||||
size="xs"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</MTable.Td>
|
||||
)}
|
||||
|
||||
{renderShortIcon(column)}
|
||||
</Box>
|
||||
</MTable.Th>
|
||||
))}
|
||||
{columns.map((col) => (
|
||||
<MTable.Td key={`${col.key}-${uuid()}`} {...td}>
|
||||
{/* {col.renderRow ? col.renderRow(row) : row[col.key]} */}
|
||||
|
||||
{actions && (
|
||||
<MTable.Th {...th} key={'__action_head_' + columns.length + 1}>
|
||||
{actions.title ? actions.title : 'Action'}
|
||||
</MTable.Th>
|
||||
)}
|
||||
</MTable.Tr>
|
||||
</MTable.Thead>
|
||||
<MTable.Tbody {...tbody} className="relative">
|
||||
{rowsData.length > 0 &&
|
||||
rowsData.map((row, index) => (
|
||||
<MTable.Tr {...trbody} key={row[rowKey]}>
|
||||
{showChooses && (
|
||||
<MTable.Td key={'__choose_' + index} {...td} {...chooseOptions?.defaultBodyProps}>
|
||||
{chooseOptions?.renderBody ? (
|
||||
chooseOptions.renderBody(chooses, row, (e) => handleChooseSingle(e, row))
|
||||
) : (
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
input: 'cursor-pointer',
|
||||
}}
|
||||
checked={chooses.some((choose) => JSON.stringify(choose) === JSON.stringify(row))}
|
||||
onChange={(e) => handleChooseSingle(e, row)}
|
||||
size="xs"
|
||||
{renderRow(row, col)}
|
||||
</MTable.Td>
|
||||
))}
|
||||
|
||||
{actions && (
|
||||
<MTable.Td key={'__action_' + index} {...td}>
|
||||
{actions.body(row)}
|
||||
</MTable.Td>
|
||||
)}
|
||||
</MTable.Tr>
|
||||
))}
|
||||
|
||||
{rowsData.length <= 0 &&
|
||||
(emptyDataTemplate ? (
|
||||
emptyDataTemplate
|
||||
) : (
|
||||
<MTable.Tr>
|
||||
<MTable.Td className="h-10">
|
||||
<div className="absolute border border-[#424242] border-t-0 w-full h-full top-0 left-0 flex items-center justify-center font-medium bg-[#242424]">
|
||||
<span>Empty Data</span>
|
||||
</div>
|
||||
</MTable.Td>
|
||||
</MTable.Tr>
|
||||
))}
|
||||
|
||||
{showLoading &&
|
||||
loading &&
|
||||
(loadingTemplate ? (
|
||||
loadingTemplate
|
||||
) : (
|
||||
<MTable.Tr>
|
||||
<MTable.Td className="h-10">
|
||||
<div className="absolute w-full h-full top-0 left-0 flex items-center justify-center font-medium bg-[rgba(0,0,0,.4)]">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="inline w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600 dark:fill-gray-300"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</MTable.Td>
|
||||
)}
|
||||
|
||||
{columns.map((col) => (
|
||||
<MTable.Td key={`${col.key}-${uuid()}`} {...td}>
|
||||
{/* {col.renderRow ? col.renderRow(row) : row[col.key]} */}
|
||||
|
||||
{renderRow(row, col)}
|
||||
</MTable.Td>
|
||||
))}
|
||||
|
||||
{actions && (
|
||||
<MTable.Td key={'__action_' + index} {...td}>
|
||||
{actions.body(row)}
|
||||
</MTable.Td>
|
||||
)}
|
||||
</MTable.Tr>
|
||||
))}
|
||||
|
||||
{rowsData.length <= 0 &&
|
||||
(emptyDataTemplate ? (
|
||||
emptyDataTemplate
|
||||
) : (
|
||||
<MTable.Tr>
|
||||
<MTable.Td className="h-10">
|
||||
<div className="absolute border border-[#424242] border-t-0 w-full h-full top-0 left-0 flex items-center justify-center font-medium bg-[#242424]">
|
||||
<span>Empty Data</span>
|
||||
</div>
|
||||
</MTable.Td>
|
||||
</MTable.Tr>
|
||||
))}
|
||||
|
||||
{showLoading &&
|
||||
loading &&
|
||||
(loadingTemplate ? (
|
||||
loadingTemplate
|
||||
) : (
|
||||
<MTable.Tr>
|
||||
<MTable.Td className="h-10">
|
||||
<div className="absolute w-full h-full top-0 left-0 flex items-center justify-center font-medium bg-[rgba(0,0,0,.4)]">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="inline w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-gray-600 dark:fill-gray-300"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</MTable.Td>
|
||||
</MTable.Tr>
|
||||
))}
|
||||
</MTable.Tbody>
|
||||
</MTable>
|
||||
</MTable.Tr>
|
||||
))}
|
||||
</MTable.Tbody>
|
||||
</MTable>
|
||||
</MTable.ScrollContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -77,3 +77,8 @@ export const removeFalsy = (data: { [key: string]: string | number }) => {
|
|||
return prev;
|
||||
}, {} 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 { useMemo, useRef, useState } from 'react';
|
||||
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 { IColumn, TRefTableFn } from '../lib/table/type';
|
||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
|
||||
|
|
@ -20,13 +20,14 @@ export default function Bids() {
|
|||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const [openedHistories, historiesModel] = useDisclosure(false);
|
||||
const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
|
||||
const [openedBid, bidModal] = useDisclosure(false);
|
||||
|
||||
const columns: IColumn<IBid>[] = [
|
||||
{
|
||||
key: 'id',
|
||||
title: 'ID',
|
||||
typeFilter: 'number',
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
typeFilter: 'text',
|
||||
},
|
||||
{
|
||||
key: 'lot_id',
|
||||
|
|
@ -38,14 +39,14 @@ export default function Bids() {
|
|||
title: 'Model',
|
||||
typeFilter: 'text',
|
||||
},
|
||||
// {
|
||||
// key: 'quantity',
|
||||
// title: 'Qty',
|
||||
// typeFilter: 'number',
|
||||
// },
|
||||
{
|
||||
key: 'quantity',
|
||||
title: 'Qty',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'step_price',
|
||||
title: 'Step price',
|
||||
key: 'plus_price',
|
||||
title: 'Plus price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
|
|
@ -58,6 +59,11 @@ export default function Bids() {
|
|||
title: 'Current price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'reserve_price',
|
||||
title: 'Reserve price',
|
||||
typeFilter: 'number',
|
||||
},
|
||||
{
|
||||
key: 'histories',
|
||||
title: 'Current bid',
|
||||
|
|
@ -107,14 +113,14 @@ export default function Bids() {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'updated_at',
|
||||
title: 'Update at',
|
||||
typeFilter: 'none',
|
||||
renderRow(row) {
|
||||
return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
||||
},
|
||||
},
|
||||
// {
|
||||
// key: 'updated_at',
|
||||
// title: 'Update at',
|
||||
// typeFilter: 'none',
|
||||
// renderRow(row) {
|
||||
// return <span className="text-sm">{formatTime(row.updated_at)}</span>;
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
||||
const handleDelete = (bid: IBid) => {
|
||||
|
|
@ -242,6 +248,15 @@ export default function Bids() {
|
|||
>
|
||||
Histories
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setClickData(row);
|
||||
historiesGraysApiModel.open();
|
||||
}}
|
||||
leftSection={<IconHistory size={14} />}
|
||||
>
|
||||
Bids
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
disabled={row.status === 'win-bid'}
|
||||
|
|
@ -293,6 +308,23 @@ export default function Bids() {
|
|||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
|
||||
<ShowHistoriesBidGraysApiModal
|
||||
onUpdated={() => {
|
||||
if (refTableFn.current?.fetchData) {
|
||||
refTableFn.current.fetchData();
|
||||
}
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
opened={openedHistoriesGraysApi}
|
||||
onClose={() => {
|
||||
historiesGraysApiModel.close();
|
||||
|
||||
setClickData(null);
|
||||
}}
|
||||
data={clickData}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import io from 'socket.io-client';
|
|||
import { WorkingPage } from '../components/dashboard';
|
||||
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, {
|
||||
autoConnect: true, // Tránh tự động kết nối khi import file
|
||||
transports: ['websocket'], // Chỉ dùng WebSocket để giảm độ trễ
|
||||
autoConnect: true,
|
||||
transports: ['websocket'],
|
||||
});
|
||||
|
||||
export default function DashBoard() {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,14 @@ export interface ITimestamp {
|
|||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"axios": "^1.8.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"nestjs-paginate": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"axios": "^1.8.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.13.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 { OutBidLogsService } from './services/out-bid-logs.service';
|
||||
import { WebBidsService } from './services/web-bids.service';
|
||||
import { BotTelegramApi } from './apis/bot-telegram.api';
|
||||
import { GraysApi } from './apis/grays.api';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -40,6 +42,8 @@ import { WebBidsService } from './services/web-bids.service';
|
|||
BidGateway,
|
||||
OutBidLogsService,
|
||||
WebBidsService,
|
||||
BotTelegramApi,
|
||||
GraysApi,
|
||||
],
|
||||
})
|
||||
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 { CreateBidDto } from '../../dto/bid/create-bid.dto';
|
||||
import { BidHistoriesService } from '../../services/bid-histories.service';
|
||||
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')
|
||||
export class AdminBidHistoriesController {
|
||||
constructor(private readonly bidHistoriesService: BidHistoriesService) {}
|
||||
constructor(
|
||||
private readonly bidHistoriesService: BidHistoriesService,
|
||||
private readonly graysApi: GraysApi,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateBidHistoryDto) {
|
||||
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) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateBidHistoryDto) {
|
||||
return this.bidHistoriesService.create(data);
|
||||
async create(@Body() data: CreateBidHistoryDto) {
|
||||
return await this.bidHistoriesService.create(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,8 @@ export class ClientUpdateBidDto {
|
|||
@IsNumber()
|
||||
@IsOptional()
|
||||
current_price: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
reserve_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ export class CreateBidDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ export class UpdateBidDto {
|
|||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ export class Bid extends Timestamp {
|
|||
lot_id: string;
|
||||
|
||||
@Column({ default: 0 })
|
||||
step_price: number;
|
||||
plus_price: number;
|
||||
|
||||
@Column({ default: 0 })
|
||||
reserve_price: number;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
@Entity('out-bid-logs')
|
||||
@Unique(['model', 'out_price'])
|
||||
export class OutBidLog extends Timestamp {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column({ unique: true, default: null, nullable: true })
|
||||
@Column({ default: null, nullable: true })
|
||||
model: string | null;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ export class BidGateway implements OnGatewayConnection {
|
|||
|
||||
async onModuleInit() {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Repository } from 'typeorm';
|
|||
import { BidHistory } from '../entities/bid-history.entity';
|
||||
import { Bid } from '../entities/bid.entity';
|
||||
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
|
||||
import { BotTelegramApi } from '../apis/bot-telegram.api';
|
||||
|
||||
@Injectable()
|
||||
export class BidHistoriesService {
|
||||
|
|
@ -19,6 +20,7 @@ export class BidHistoriesService {
|
|||
readonly bidHistoriesRepo: Repository<BidHistory>,
|
||||
@InjectRepository(Bid)
|
||||
readonly bidsRepo: Repository<Bid>,
|
||||
private readonly botTelegramApi: BotTelegramApi,
|
||||
) {}
|
||||
|
||||
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' });
|
||||
|
||||
throw new BadRequestException(
|
||||
|
|
@ -70,6 +72,8 @@ export class BidHistoriesService {
|
|||
this.bidsRepo.update(bid_id, { first_bid: false });
|
||||
}
|
||||
|
||||
this.botTelegramApi.sendBidInfo({ ...bid, histories: response });
|
||||
|
||||
return AppResponse.toResponse(plainToClass(BidHistory, response));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,10 +127,10 @@ export class BidsService {
|
|||
|
||||
const result = await this.bidsRepo.update(id, {
|
||||
...data,
|
||||
status:
|
||||
prev.max_price + prev.step_price > data.max_price
|
||||
? 'out-bid'
|
||||
: prev.status,
|
||||
// status:
|
||||
// prev.max_price + prev.plus_price > data.max_price
|
||||
// ? 'out-bid'
|
||||
// : prev.status,
|
||||
});
|
||||
|
||||
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(
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (data.current_price > bid.max_price + bid.step_price) {
|
||||
if (data.current_price > bid.max_price + bid.plus_price) {
|
||||
bid.status = 'out-bid';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,17 @@ export class OutBidLogsService {
|
|||
}
|
||||
|
||||
async create(data: CreateOutBidLogDto[]) {
|
||||
const result = await this.outbidLogRepo.upsert(data, {
|
||||
conflictPaths: ['model', 'id'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
});
|
||||
try {
|
||||
const result = await this.outbidLogRepo.upsert(data, {
|
||||
conflictPaths: ['model', 'lot_id'],
|
||||
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);
|
||||
}
|
||||
|
||||
async emitAccountUpdate(id: WebBid['id']) {
|
||||
const data = await this.webBidRepo.findOne({ where: { id } });
|
||||
|
||||
this.eventEmitter.emit('web.updated', data || null);
|
||||
}
|
||||
|
||||
async createByUrl(url: string) {
|
||||
const originUrl = extractDomain(url);
|
||||
|
||||
|
|
@ -131,6 +137,9 @@ export class WebBidsService {
|
|||
|
||||
if (!result) throw new BadRequestException(false);
|
||||
|
||||
if (data.password !== prev.password || data.username !== prev.username) {
|
||||
this.emitAccountUpdate(prev.id);
|
||||
}
|
||||
this.emitAllBidEvent();
|
||||
|
||||
return AppResponse.toResponse(true);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
database: configService.get<string>('DB_NAME'),
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeMarkdownV2(text: string) {
|
||||
return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'dotenv/config';
|
||||
import _ from 'lodash';
|
||||
import { io } from 'socket.io-client';
|
||||
import { GrayApiBid } from './models/grays.com/grays-api-bid.js';
|
||||
import { GraysProductBid } from './models/grays.com/grays-product-bid.js';
|
||||
import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js';
|
||||
import browser from './system/browser.js';
|
||||
import configs from './system/config.js';
|
||||
import { isTimeReached, safeClosePage } from './system/utils.js';
|
||||
import browser from './system/browser.js';
|
||||
|
||||
let MANAGER_BIDS = [];
|
||||
|
||||
|
|
@ -12,31 +12,7 @@ let _INTERVAL_TRACKING_ID = null;
|
|||
let _CLEAR_LAZY_TAB_ID = null;
|
||||
let _WORK_TRACKING_ID = null;
|
||||
|
||||
const handleCloseRemoveProduct = (data) => {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
const handleUpdateProductTabs = (data) => {
|
||||
if (!Array.isArray(data)) {
|
||||
|
|
@ -75,6 +51,7 @@ const handleUpdateProductTabs = (data) => {
|
|||
const tracking = async () => {
|
||||
if (_INTERVAL_TRACKING_ID) {
|
||||
clearInterval(_INTERVAL_TRACKING_ID);
|
||||
_INTERVAL_TRACKING_ID = null;
|
||||
}
|
||||
|
||||
_INTERVAL_TRACKING_ID = setInterval(async () => {
|
||||
|
|
@ -121,42 +98,129 @@ const tracking = async () => {
|
|||
}, 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 () => {
|
||||
if (_CLEAR_LAZY_TAB_ID) {
|
||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||
_CLEAR_LAZY_TAB_ID = null;
|
||||
}
|
||||
|
||||
_CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||
const pages = await browser.pages();
|
||||
try {
|
||||
_CLEAR_LAZY_TAB_ID = setInterval(async () => {
|
||||
if (!global.IS_CLEANING) return;
|
||||
|
||||
// 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({ activeUrls });
|
||||
|
||||
for (const page of pages) {
|
||||
const pageUrl = page.url();
|
||||
|
||||
if (!activeUrls.includes(pageUrl)) {
|
||||
console.log(`Closing unused tab: ${pageUrl}`);
|
||||
await page.close();
|
||||
if (!browser) {
|
||||
console.warn('⚠️ Browser is not available or disconnected.');
|
||||
clearInterval(_CLEAR_LAZY_TAB_ID);
|
||||
_CLEAR_LAZY_TAB_ID = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, configs.AUTO_TRACKING_CLEANING);
|
||||
|
||||
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();
|
||||
|
||||
// 🔥 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 {
|
||||
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: ', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const workTracking = () => {
|
||||
if (_WORK_TRACKING_ID) {
|
||||
clearInterval(_WORK_TRACKING_ID);
|
||||
}
|
||||
|
||||
_WORK_TRACKING_ID = setInterval(() => {
|
||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
||||
|
||||
for (const item of activeData) {
|
||||
item.handleTakeWorkSnapshot();
|
||||
try {
|
||||
if (_WORK_TRACKING_ID) {
|
||||
clearInterval(_WORK_TRACKING_ID);
|
||||
_WORK_TRACKING_ID = null;
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
_WORK_TRACKING_ID = setInterval(() => {
|
||||
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
|
||||
|
||||
for (const item of activeData) {
|
||||
if (item.page_context && !item.page_context.isClosed()) {
|
||||
item.handleTakeWorkSnapshot();
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
} catch (error) {
|
||||
console.log('Loi oi day');
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
|
|
@ -180,6 +244,26 @@ const workTracking = () => {
|
|||
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
|
||||
tracking();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
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 { sanitizeFileName } from '../system/utils.js';
|
||||
import { Account } from './account.js';
|
||||
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export class ApiBid extends Bid {
|
||||
id;
|
||||
|
|
@ -93,7 +92,7 @@ export class ApiBid extends Bid {
|
|||
async restoreContext() {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import path from 'path';
|
||||
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
|
||||
import configs from '../../system/config.js';
|
||||
import CONSTANTS from '../../system/constants.js';
|
||||
import { delay, extractNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js';
|
||||
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
|
||||
import { ApiBid } from '../api-bid.js';
|
||||
import * as fs from 'fs';
|
||||
import fs from 'fs';
|
||||
|
||||
export class GrayApiBid extends ApiBid {
|
||||
retry_login = 0;
|
||||
|
|
@ -129,20 +129,25 @@ export class GrayApiBid extends ApiBid {
|
|||
async handleLogin() {
|
||||
const page = this.page_context;
|
||||
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
// 🔍 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.');
|
||||
global.IS_CLEANING = true;
|
||||
|
||||
this.retry_login = 0; // Reset retry count
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔑 Starting login process...');
|
||||
|
||||
await page.type('input[name="username"]', this.username, { delay: 100 });
|
||||
await page.type('input[name="password"]', this.password, { delay: 150 });
|
||||
await page.click('#loginButton');
|
||||
global.IS_CLEANING = false;
|
||||
|
||||
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
|
||||
|
|
@ -151,6 +156,7 @@ export class GrayApiBid extends ApiBid {
|
|||
if (!(await page.$('input[name="username"]'))) {
|
||||
console.log('✅ Login successful!');
|
||||
this.retry_login = 0; // Reset retry count after success
|
||||
global.IS_CLEANING = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class GraysProductBid extends ProductBid {
|
|||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
this.lot_id = response.lot_id;
|
||||
|
|
@ -181,9 +181,9 @@ export class GraysProductBid extends ProductBid {
|
|||
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;
|
||||
|
||||
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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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, sanitizeFileName } from '../system/utils.js';
|
||||
import { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
|
||||
export class ProductBid extends Bid {
|
||||
|
|
@ -11,7 +11,7 @@ export class ProductBid extends Bid {
|
|||
max_price;
|
||||
model;
|
||||
lot_id;
|
||||
step_price;
|
||||
plus_price;
|
||||
close_time;
|
||||
first_bid;
|
||||
quantity;
|
||||
|
|
@ -23,11 +23,12 @@ export class ProductBid extends Bid {
|
|||
web_bid;
|
||||
current_price;
|
||||
name;
|
||||
reserve_price;
|
||||
|
||||
constructor({
|
||||
url,
|
||||
max_price,
|
||||
step_price,
|
||||
plus_price,
|
||||
model,
|
||||
first_bid = false,
|
||||
id,
|
||||
|
|
@ -40,12 +41,13 @@ export class ProductBid extends Bid {
|
|||
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.step_price = step_price || 0;
|
||||
this.plus_price = plus_price || 0;
|
||||
this.first_bid = first_bid;
|
||||
this.id = id;
|
||||
this.created_at = created_at;
|
||||
|
|
@ -58,12 +60,13 @@ export class ProductBid extends Bid {
|
|||
this.web_bid = web_bid;
|
||||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
}
|
||||
|
||||
setNewData({
|
||||
url,
|
||||
max_price,
|
||||
step_price,
|
||||
plus_price,
|
||||
model,
|
||||
first_bid = false,
|
||||
id,
|
||||
|
|
@ -76,11 +79,12 @@ export class ProductBid extends Bid {
|
|||
start_bid_time,
|
||||
web_bid,
|
||||
current_price,
|
||||
reserve_price,
|
||||
name,
|
||||
}) {
|
||||
this.max_price = max_price || 0;
|
||||
this.model = model;
|
||||
this.step_price = step_price || 0;
|
||||
this.plus_price = plus_price || 0;
|
||||
this.first_bid = first_bid;
|
||||
this.id = id;
|
||||
this.created_at = created_at;
|
||||
|
|
@ -94,6 +98,7 @@ export class ProductBid extends Bid {
|
|||
this.url = url;
|
||||
this.current_price = current_price;
|
||||
this.name = name;
|
||||
this.reserve_price = reserve_price;
|
||||
}
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
|
|
@ -107,7 +112,7 @@ export class ProductBid extends Bid {
|
|||
const statusInit = await this.restoreContext(context);
|
||||
|
||||
if (!statusInit) {
|
||||
console.log(`⚠️ Restore failed. Restart count`);
|
||||
console.log(`⚠️ Restore failed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +122,7 @@ export class ProductBid extends Bid {
|
|||
};
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"puppeteer": "^24.4.0",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
|
|
@ -480,6 +481,18 @@
|
|||
"integrity": "sha512-yRtvFD8Oyk7C9Os3GmnFZLu53yAfsnyw1s+mLmHHUK0GQEc9zthHWvS1r67Zqzm5t7v56PILHIVZ7kmFMaL2yQ==",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"lodash": "^4.17.21",
|
||||
"puppeteer": "^24.4.0",
|
||||
"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) {
|
||||
console.log('❌ UPDATE FAILURE');
|
||||
console.log('❌ UPDATE FAILURE (UPDATE BID)');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log('❌ ERROR IN SERVER: ', error);
|
||||
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import ax from 'axios';
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|||
|
||||
puppeteer.use(StealthPlugin());
|
||||
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
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
const configs = {
|
||||
AUTO_TRACKING_DELAY: 5000,
|
||||
AUTO_TRACKING_CLEANING: 10000,
|
||||
SOCKET_URL: 'http://localhost:4000',
|
||||
SOCKET_URL: process.env.SOCKET_URL,
|
||||
WEB_URLS: {
|
||||
GRAYS: `https://www.grays.com`,
|
||||
},
|
||||
WEB_CONFIGS: {
|
||||
GRAYS: {
|
||||
AUTO_CALL_API_TO_TRACKING: 10000,
|
||||
AUTO_CALL_API_TO_TRACKING: 3000,
|
||||
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}`);
|
||||
}
|
||||
|
||||
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
|
||||
await page.screenshot({ path: filePath, fullPage: true });
|
||||
|
|
@ -42,13 +42,20 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_
|
|||
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function safeClosePage(item) {
|
||||
const page = item.page_context;
|
||||
try {
|
||||
const page = item.page_context;
|
||||
|
||||
if (!page?.isClosed() && page?.close) {
|
||||
await page.close();
|
||||
if (!page?.isClosed() && page?.close) {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
item.page_context = undefined;
|
||||
if (item?.page_context) {
|
||||
item.page_context = undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Can't close item: " + item.id);
|
||||
}
|
||||
|
||||
item.page_context = undefined;
|
||||
}
|
||||
|
||||
export function isTimeReached(targetTime) {
|
||||
|
|
@ -68,3 +75,7 @@ export function extractNumber(str) {
|
|||
export const sanitizeFileName = (url) => {
|
||||
return url.replace(/[:\/]/g, '_');
|
||||
};
|
||||
|
||||
export const getPathProfile = (origin_url) => {
|
||||
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue