pickxel and fix login
This commit is contained in:
parent
b13712a317
commit
65ae7da6e6
|
|
@ -1,92 +1,110 @@
|
|||
import { generateNestParams, handleError, handleSuccess } from '.';
|
||||
import axios from '../lib/axios';
|
||||
import { IWebBid } from '../system/type';
|
||||
import { removeFalsyValues } from '../utils';
|
||||
import { generateNestParams, handleError, handleSuccess } from ".";
|
||||
import axios from "../lib/axios";
|
||||
import { IWebBid } from "../system/type";
|
||||
import { removeFalsyValues } from "../utils";
|
||||
|
||||
const BASE_URL = 'web-bids';
|
||||
const BASE_URL = "web-bids";
|
||||
|
||||
export const getWebBids = async (params: Record<string, string | number>) => {
|
||||
return await axios({
|
||||
url: BASE_URL,
|
||||
params: generateNestParams(params),
|
||||
withCredentials: true,
|
||||
method: 'GET',
|
||||
});
|
||||
return await axios({
|
||||
url: BASE_URL,
|
||||
params: generateNestParams(params),
|
||||
withCredentials: true,
|
||||
method: "GET",
|
||||
});
|
||||
};
|
||||
|
||||
export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
|
||||
const newData = removeFalsyValues(bid);
|
||||
export const createWebBid = async (
|
||||
bid: Omit<IWebBid, "id" | "created_at" | "updated_at" | "is_system_account">
|
||||
) => {
|
||||
const newData = removeFalsyValues(bid);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: BASE_URL,
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
data: newData,
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: BASE_URL,
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
data: newData,
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateWebBid = async (bid: Partial<IWebBid>) => {
|
||||
const { url, password, username, origin_url, active } = removeFalsyValues(bid, ['active']);
|
||||
const {
|
||||
url,
|
||||
password,
|
||||
username,
|
||||
origin_url,
|
||||
active,
|
||||
arrival_offset_seconds,
|
||||
// early_login_seconds
|
||||
} = removeFalsyValues(bid, ["active"]);
|
||||
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/` + bid.id,
|
||||
withCredentials: true,
|
||||
method: 'PUT',
|
||||
data: { url, password, username, origin_url, active },
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/` + bid.id,
|
||||
withCredentials: true,
|
||||
method: "PUT",
|
||||
data: {
|
||||
url,
|
||||
password,
|
||||
username,
|
||||
origin_url,
|
||||
active,
|
||||
arrival_offset_seconds,
|
||||
// early_login_seconds
|
||||
},
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWebBid = async (web: IWebBid) => {
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/` + web.id,
|
||||
withCredentials: true,
|
||||
method: 'DELETE',
|
||||
});
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/` + web.id,
|
||||
withCredentials: true,
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const deletesWebBid = async (web: IWebBid[]) => {
|
||||
const ids = web.reduce((prev, cur) => {
|
||||
prev.push(cur.id);
|
||||
return prev;
|
||||
}, [] as number[]);
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/deletes`,
|
||||
withCredentials: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
const ids = web.reduce((prev, cur) => {
|
||||
prev.push(cur.id);
|
||||
return prev;
|
||||
}, [] as number[]);
|
||||
try {
|
||||
const { data } = await axios({
|
||||
url: `${BASE_URL}/deletes`,
|
||||
withCredentials: true,
|
||||
method: "POST",
|
||||
data: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
|
||||
handleSuccess(data);
|
||||
handleSuccess(data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...pr
|
|||
<Table.Tr key={index}>
|
||||
<Table.Td>{element['bidderAnonName']}</Table.Td>
|
||||
<Table.Td>{element['actualBid']}</Table.Td>
|
||||
<<<<<<< HEAD
|
||||
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())}</Table.Td>
|
||||
=======
|
||||
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
</Table.Tr>
|
||||
));
|
||||
}, [histories]);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH
|
|||
<Table.Tr key={element.id}>
|
||||
<Table.Td>{element.id}</Table.Td>
|
||||
<Table.Td>{element.price}</Table.Td>
|
||||
<Table.Td>{formatTime(element.created_at, 'DD/MM/YYYY HH:MM')}</Table.Td>
|
||||
<Table.Td>{formatTime(new Date(element.created_at).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +1,196 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { createWebBid, updateWebBid } from '../../apis/web-bid';
|
||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
|
||||
import { IWebBid } from '../../system/type';
|
||||
import { extractDomain } from '../../utils';
|
||||
import {
|
||||
Button,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
ModalProps,
|
||||
NumberInput,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { createWebBid, updateWebBid } from "../../apis/web-bid";
|
||||
import { useConfirmStore } from "../../lib/zustand/use-confirm";
|
||||
import { IWebBid } from "../../system/type";
|
||||
import { extractDomain } from "../../utils";
|
||||
export interface IWebBidModelProps extends ModalProps {
|
||||
data: IWebBid | null;
|
||||
onUpdated?: () => void;
|
||||
data: IWebBid | null;
|
||||
onUpdated?: () => void;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
||||
url: z.string({ message: "Url is required" }).url("Invalid url format"),
|
||||
arrival_offset_seconds: z
|
||||
.number({ message: "Arrival offset seconds is required" })
|
||||
.refine((val) => val >= 60, {
|
||||
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
|
||||
}),
|
||||
early_login_seconds: z
|
||||
.number({ message: "Early login seconds is required" })
|
||||
.refine((val) => val >= 600, {
|
||||
message: "Early login seconds must be at least 600 seconds (10 minute)",
|
||||
}),
|
||||
};
|
||||
|
||||
export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(z.object(schema)),
|
||||
});
|
||||
export default function WebBidModal({
|
||||
data,
|
||||
onUpdated,
|
||||
...props
|
||||
}: IWebBidModelProps) {
|
||||
const form = useForm({
|
||||
validate: zodResolver(z.object(schema)),
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const prevData = useRef<IWebBid | null>(data);
|
||||
const prevData = useRef<IWebBid | null>(data);
|
||||
|
||||
const { setConfirm } = useConfirmStore();
|
||||
const { setConfirm } = useConfirmStore();
|
||||
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: 'Update ?',
|
||||
message: `This web will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
if (data) {
|
||||
setConfirm({
|
||||
title: "Update ?",
|
||||
message: `This web will be update`,
|
||||
handleOk: async () => {
|
||||
setLoading(true);
|
||||
console.log(
|
||||
"%csrc/components/web-bid/web-bid-modal.tsx:54 values",
|
||||
"color: #007acc;",
|
||||
values
|
||||
);
|
||||
const result = await updateWebBid(values);
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: 'blue',
|
||||
value: 'Update',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { url, origin_url } = values;
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
},
|
||||
okButton: {
|
||||
color: "blue",
|
||||
value: "Update",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
|
||||
|
||||
setLoading(true);
|
||||
const result = await createWebBid({ url, origin_url } as IWebBid);
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const result = await createWebBid({
|
||||
url,
|
||||
origin_url,
|
||||
arrival_offset_seconds,
|
||||
early_login_seconds
|
||||
} as IWebBid);
|
||||
setLoading(false);
|
||||
|
||||
if (!result) return;
|
||||
if (!result) return;
|
||||
|
||||
props.onClose();
|
||||
props.onClose();
|
||||
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (onUpdated) {
|
||||
onUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
if (!data) return;
|
||||
|
||||
form.setValues(data);
|
||||
form.setValues(data);
|
||||
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
prevData.current = data;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
useEffect(() => {
|
||||
if (!props.opened) {
|
||||
form.reset();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened]);
|
||||
|
||||
useEffect(() => {
|
||||
if (form.values?.url) {
|
||||
form.setFieldValue('origin_url', extractDomain(form.values.url));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form.values]);
|
||||
useEffect(() => {
|
||||
if (form.values?.url) {
|
||||
form.setFieldValue("origin_url", extractDomain(form.values.url));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form.values]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: '!flex !item-center !justify-center w-full',
|
||||
}}
|
||||
{...props}
|
||||
size={'xl'}
|
||||
title={<span className="text-xl font-bold">Web</span>}
|
||||
centered
|
||||
return (
|
||||
<Modal
|
||||
className="relative"
|
||||
classNames={{
|
||||
header: "!flex !item-center !justify-center w-full",
|
||||
}}
|
||||
{...props}
|
||||
size={"xl"}
|
||||
title={<span className="text-xl font-bold">Web</span>}
|
||||
centered
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
className="grid grid-cols-2 gap-2.5"
|
||||
>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Domain"
|
||||
{...form.getInputProps("origin_url")}
|
||||
/>
|
||||
<TextInput
|
||||
withAsterisk
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label="Tracking url"
|
||||
{...form.getInputProps("url")}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label={`Arrival offset seconds (${
|
||||
form.getValues()["arrival_offset_seconds"] / 60
|
||||
} minutes)`}
|
||||
placeholder="msg: 300"
|
||||
{...form.getInputProps("arrival_offset_seconds")}
|
||||
/>
|
||||
{/* <NumberInput
|
||||
description="Note: that only integer minutes are accepted."
|
||||
className="col-span-2"
|
||||
size="sm"
|
||||
label={`Early login seconds (${
|
||||
form.getValues()["early_login_seconds"] / 60
|
||||
} minutes)`}
|
||||
placeholder="msg: 600"
|
||||
{...form.getInputProps("early_login_seconds")}
|
||||
/> */}
|
||||
|
||||
<Button
|
||||
disabled={_.isEqual(form.getValues(), prevData.current)}
|
||||
className="col-span-2"
|
||||
type="submit"
|
||||
fullWidth
|
||||
size="sm"
|
||||
mt="md"
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
|
||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
|
||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
|
||||
{data ? "Update" : "Create"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
||||
{data ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
|
||||
</Modal>
|
||||
);
|
||||
<LoadingOverlay
|
||||
visible={loading}
|
||||
zIndex={1000}
|
||||
overlayProps={{ blur: 2 }}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,7 @@ export interface ITimestamp {
|
|||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
status: 'biding' | 'out-bid' | 'win-bid';
|
||||
histories: IHistory[];
|
||||
web_bid: IWebBid;
|
||||
}
|
||||
|
||||
|
||||
export interface IHistory extends ITimestamp {
|
||||
id: number;
|
||||
|
|
@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp {
|
|||
username: string | null;
|
||||
password: string | null;
|
||||
active: boolean;
|
||||
arrival_offset_seconds: number;
|
||||
early_login_seconds: number;
|
||||
children: IBid[];
|
||||
}
|
||||
|
||||
export interface IBid extends ITimestamp {
|
||||
id: number;
|
||||
max_price: number;
|
||||
reserve_price: number;
|
||||
current_price: number;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
url: string;
|
||||
model: string;
|
||||
lot_id: string;
|
||||
plus_price: number;
|
||||
close_time: string | null;
|
||||
start_bid_time: string | null;
|
||||
first_bid: boolean;
|
||||
status: 'biding' | 'out-bid' | 'win-bid';
|
||||
histories: IHistory[];
|
||||
web_bid: IWebBid;
|
||||
}
|
||||
|
||||
export interface IPermission extends ITimestamp {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import moment from "moment";
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import { IWebBid } from "../system/type";
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
export function cn(...args: ClassValue[]) {
|
||||
return twMerge(clsx(args));
|
||||
}
|
||||
|
|
@ -150,4 +154,31 @@ export function stringToColor(str: string): string {
|
|||
const hash = hashStringToInt(str);
|
||||
const index = hash % colorPalette.length;
|
||||
return colorPalette[index];
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
}
|
||||
|
||||
export function findEarlyLoginTime(webBid: IWebBid): string | null {
|
||||
const now = new Date();
|
||||
|
||||
// Bước 1: Lọc ra những bid có close_time hợp lệ
|
||||
const validChildren = webBid.children.filter(child => child.close_time);
|
||||
|
||||
if (validChildren.length === 0) return null;
|
||||
|
||||
// Bước 2: Tìm bid có close_time gần hiện tại nhất
|
||||
const closestBid = validChildren.reduce((closest, current) => {
|
||||
const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime());
|
||||
const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime());
|
||||
return currentDiff < closestDiff ? current : closest;
|
||||
});
|
||||
|
||||
if (!closestBid.close_time) return null;
|
||||
|
||||
// Bước 3: Tính toán thời gian login sớm
|
||||
const closeTime = new Date(closestBid.close_time);
|
||||
closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_login_seconds || 0));
|
||||
|
||||
return closeTime.toISOString();
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
{"createdAt":1745827424853}
|
||||
<<<<<<< HEAD
|
||||
{"createdAt":1745827424853}
|
||||
=======
|
||||
{"createdAt":1746413672600}
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export class GraysApi {
|
|||
|
||||
switch(bid.web_bid.origin_url){
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
// GRAYS
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
case 'https://www.grays.com': {
|
||||
const response = await axios({
|
||||
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
||||
|
|
@ -32,6 +36,11 @@ export class GraysApi {
|
|||
|
||||
return AppResponse.toResponse([])
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
// PICKLES
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
case 'https://www.pickles.com.au': {
|
||||
|
||||
const response = await axios({
|
||||
|
|
@ -39,7 +48,11 @@ export class GraysApi {
|
|||
});
|
||||
|
||||
if (response.data) {
|
||||
<<<<<<< HEAD
|
||||
return AppResponse.toResponse(response.data);
|
||||
=======
|
||||
return AppResponse.toResponse(response.data.Bids);
|
||||
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||
}
|
||||
|
||||
return AppResponse.toResponse([])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator';
|
||||
import { IsBoolean, IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator';
|
||||
|
||||
export class UpdateWebBidDto {
|
||||
@IsUrl()
|
||||
|
|
@ -9,6 +9,16 @@ export class UpdateWebBidDto {
|
|||
@IsOptional()
|
||||
url: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(60)
|
||||
@IsOptional()
|
||||
arrival_offset_seconds: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(600)
|
||||
@IsOptional()
|
||||
early_login_seconds: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
username: string;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ export class WebBid extends Timestamp {
|
|||
@Column({ default: null, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ default: 300 })
|
||||
arrival_offset_seconds: number;
|
||||
|
||||
@Column({ default: 600 })
|
||||
early_login_seconds: number;
|
||||
|
||||
@Column({ default: null, nullable: true })
|
||||
@Exclude()
|
||||
password: string;
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ export class BidsService {
|
|||
if (!bid.close_time && !bid.start_bid_time) {
|
||||
// Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
|
||||
// bid.start_bid_time = new Date().toUTCString();
|
||||
bid.start_bid_time = subtractMinutes(close_time, 5);
|
||||
bid.start_bid_time = subtractMinutes(close_time, bid.web_bid.arrival_offset_seconds/ 60);
|
||||
}
|
||||
|
||||
// Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export class WebBidsService {
|
|||
async emitAccountUpdate(id: WebBid['id']) {
|
||||
const data = await this.webBidRepo.findOne({
|
||||
where: { id, children: { status: 'biding' } },
|
||||
relations: { children: true },
|
||||
relations: { children: { web_bid: true } },
|
||||
});
|
||||
|
||||
this.eventEmitter.emit(Event.WEB_UPDATED, data || null);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ import {
|
|||
} from "./service/app-service.js";
|
||||
import browser from "./system/browser.js";
|
||||
import configs from "./system/config.js";
|
||||
import { delay, isTimeReached, safeClosePage } from "./system/utils.js";
|
||||
import {
|
||||
delay,
|
||||
findEarlyLoginTime,
|
||||
isTimeReached,
|
||||
safeClosePage,
|
||||
} from "./system/utils.js";
|
||||
import { updateLoginStatus } from "./system/apis/bid.js";
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
|
|
@ -53,6 +58,59 @@ const handleUpdateProductTabs = (data) => {
|
|||
MANAGER_BIDS = newDataManager;
|
||||
};
|
||||
|
||||
const addProductTab = (data) => {
|
||||
if (
|
||||
typeof data !== "object" ||
|
||||
data === null ||
|
||||
!Array.isArray(data.children)
|
||||
) {
|
||||
console.warn("Data must be an object with a children array");
|
||||
return;
|
||||
}
|
||||
|
||||
const { children, ...web } = data;
|
||||
|
||||
if (children.length === 0) {
|
||||
console.warn(
|
||||
`⚠️ No children found for bid id ${web.id}, skipping addProductTab`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
|
||||
const prevApiBid = managerBidMap.get(web.id);
|
||||
|
||||
if (prevApiBid) {
|
||||
// Cập nhật
|
||||
const updatedChildren = prevApiBid.children;
|
||||
|
||||
children.forEach((newChild) => {
|
||||
const existingChildIndex = updatedChildren.findIndex(
|
||||
(c) => c.id === newChild.id
|
||||
);
|
||||
|
||||
if (existingChildIndex !== -1) {
|
||||
updatedChildren[existingChildIndex].setNewData(newChild);
|
||||
} else {
|
||||
updatedChildren.push(createBidProduct(web, newChild));
|
||||
}
|
||||
});
|
||||
|
||||
prevApiBid.setNewData({ ...web, children: updatedChildren });
|
||||
} else {
|
||||
// Tạo mới
|
||||
const newChildren = children.map((item) => createBidProduct(web, item));
|
||||
const newApiBid = createApiBid({ ...web, children: newChildren });
|
||||
|
||||
MANAGER_BIDS.push(newApiBid);
|
||||
|
||||
console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", {
|
||||
MANAGER_BIDS,
|
||||
children: newChildren,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const tracking = async () => {
|
||||
console.log("🚀 Tracking process started...");
|
||||
|
||||
|
|
@ -68,6 +126,20 @@ const tracking = async () => {
|
|||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(
|
||||
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
||||
console.log(`🎧 Listening to events for close login: ${apiBid.id}`);
|
||||
return (apiBid.onCloseLogin = (data) => {
|
||||
// Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại
|
||||
MANAGER_BIDS = MANAGER_BIDS.filter(
|
||||
(item) => item.id !== data.id && item.type !== data.type
|
||||
);
|
||||
|
||||
addProductTab(data);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
Promise.allSettled(
|
||||
productTabs.map(async (productTab) => {
|
||||
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
||||
|
|
@ -159,6 +231,72 @@ const tracking = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// const clearLazyTab = async () => {
|
||||
// if (!global.IS_CLEANING) {
|
||||
// console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!browser) {
|
||||
// console.warn("⚠️ Browser is not available or disconnected.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const pages = await browser.pages();
|
||||
|
||||
// // Lấy danh sách URL từ flattenedArray
|
||||
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||
// item.url,
|
||||
// ...item.children.map((child) => child.url),
|
||||
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
|
||||
// console.log(
|
||||
// "🔍 Page URLs:",
|
||||
// pages.map((page) => page.url())
|
||||
// );
|
||||
|
||||
// for (const page of pages) {
|
||||
// const pageUrl = page.url();
|
||||
|
||||
// if (!pageUrl || pageUrl === "about:blank") continue;
|
||||
|
||||
// if (!activeUrls.includes(pageUrl)) {
|
||||
// if (!page.isClosed() && browser.isConnected()) {
|
||||
// try {
|
||||
// const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
// .map((i) => ({
|
||||
// current_url: i.page_context.url(),
|
||||
// data: i,
|
||||
// }))
|
||||
// .find((j) => j.current_url === pageUrl);
|
||||
|
||||
// console.log(bidData);
|
||||
|
||||
// if (bidData && bidData.data) {
|
||||
// await safeClosePage(bidData.data);
|
||||
// } else {
|
||||
// // 👇 Wrap close with timeout + error catch
|
||||
// await Promise.race([
|
||||
// page.close(),
|
||||
// new Promise((_, reject) =>
|
||||
// setTimeout(() => reject(new Error("Close timeout")), 3000)
|
||||
// ),
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
// } catch (err) {
|
||||
// console.warn(`⚠️ Error closing tab ${pageUrl}: ${err.message}`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("❌ Error in clearLazyTab:", err.message);
|
||||
// }
|
||||
// };
|
||||
|
||||
const clearLazyTab = async () => {
|
||||
if (!global.IS_CLEANING) {
|
||||
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
||||
|
|
@ -172,47 +310,53 @@ const clearLazyTab = async () => {
|
|||
|
||||
try {
|
||||
const pages = await browser.pages();
|
||||
console.log("🔍 Found pages:", pages.length);
|
||||
|
||||
// Lấy danh sách URL từ flattenedArray
|
||||
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||
item.url,
|
||||
...item.children.map((child) => child.url),
|
||||
]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
||||
|
||||
console.log(
|
||||
"🔍 Page URLs:",
|
||||
pages.map((page) => page.url())
|
||||
);
|
||||
]).filter(Boolean);
|
||||
|
||||
for (const page of pages) {
|
||||
const pageUrl = page.url();
|
||||
try {
|
||||
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
|
||||
|
||||
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
|
||||
if (!pageUrl || pageUrl === "about:blank") continue;
|
||||
const pageUrl = page.url();
|
||||
|
||||
if (!activeUrls.includes(pageUrl)) {
|
||||
if (!page.isClosed() && browser.isConnected()) {
|
||||
if (!pageUrl || pageUrl === "about:blank") continue;
|
||||
if (activeUrls.includes(pageUrl)) continue;
|
||||
|
||||
page.removeAllListeners();
|
||||
|
||||
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
||||
|
||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
.map((i) => ({
|
||||
current_url: i.page_context.url(),
|
||||
data: i,
|
||||
}))
|
||||
.find((j) => j.current_url === pageUrl);
|
||||
|
||||
if (bidData && bidData.data) {
|
||||
await safeClosePage(bidData.data);
|
||||
} else {
|
||||
try {
|
||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
||||
.map((i) => ({
|
||||
current_url: i.page_context.url(),
|
||||
data: i,
|
||||
}))
|
||||
.find((j) => j.current_url === pageUrl);
|
||||
|
||||
console.log(bidData);
|
||||
|
||||
if (bidData && bidData.data) {
|
||||
await safeClosePage(bidData.data);
|
||||
} else {
|
||||
await page.close();
|
||||
}
|
||||
|
||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
|
||||
await Promise.race([
|
||||
page.close(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Close timeout")), 3000)
|
||||
),
|
||||
]);
|
||||
} catch (closeErr) {
|
||||
console.warn(
|
||||
`⚠️ Error closing page ${pageUrl}: ${closeErr.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Closed page: ${pageUrl}`);
|
||||
} catch (pageErr) {
|
||||
console.warn(`⚠️ Error handling page: ${pageErr.message}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -343,6 +487,10 @@ const trackingLoginStatus = async () => {
|
|||
|
||||
await safeClosePage(tab);
|
||||
|
||||
MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id);
|
||||
|
||||
addProductTab(data);
|
||||
|
||||
global.IS_CLEANING = true;
|
||||
} else {
|
||||
console.log("⚠️ No profile found to delete.");
|
||||
|
|
|
|||
|
|
@ -1,106 +1,144 @@
|
|||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import BID_TYPE from '../system/bid-type.js';
|
||||
import browser from '../system/browser.js';
|
||||
import CONSTANTS from '../system/constants.js';
|
||||
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
|
||||
import { Bid } from './bid.js';
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
import BID_TYPE from "../system/bid-type.js";
|
||||
import browser from "../system/browser.js";
|
||||
import CONSTANTS from "../system/constants.js";
|
||||
import {
|
||||
findEarlyLoginTime,
|
||||
getPathProfile,
|
||||
isTimeReached,
|
||||
sanitizeFileName,
|
||||
} from "../system/utils.js";
|
||||
import { Bid } from "./bid.js";
|
||||
|
||||
export class ApiBid extends Bid {
|
||||
id;
|
||||
account;
|
||||
children = [];
|
||||
children_processing = [];
|
||||
created_at;
|
||||
updated_at;
|
||||
origin_url;
|
||||
active;
|
||||
browser_context;
|
||||
username;
|
||||
password;
|
||||
id;
|
||||
account;
|
||||
children = [];
|
||||
children_processing = [];
|
||||
created_at;
|
||||
updated_at;
|
||||
origin_url;
|
||||
active;
|
||||
browser_context;
|
||||
username;
|
||||
password;
|
||||
|
||||
constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
|
||||
super(BID_TYPE.API_BID, url);
|
||||
constructor({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
id,
|
||||
children,
|
||||
created_at,
|
||||
updated_at,
|
||||
origin_url,
|
||||
active,
|
||||
}) {
|
||||
super(BID_TYPE.API_BID, url);
|
||||
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.children = children;
|
||||
this.origin_url = origin_url;
|
||||
this.active = active;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.id = id;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.children = children;
|
||||
this.origin_url = origin_url;
|
||||
this.active = active;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
setNewData({
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
id,
|
||||
children,
|
||||
created_at,
|
||||
updated_at,
|
||||
origin_url,
|
||||
active,
|
||||
}) {
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.children = children;
|
||||
this.origin_url = origin_url;
|
||||
this.active = active;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
this.browser_context = await browser.createBrowserContext();
|
||||
|
||||
const page = await this.browser_context.newPage();
|
||||
|
||||
this.page_context = page;
|
||||
|
||||
await this.restoreContext();
|
||||
};
|
||||
|
||||
listen_events = async () => {
|
||||
if (this.page_context) return;
|
||||
|
||||
await this.puppeteer_connect();
|
||||
|
||||
await this.action();
|
||||
|
||||
await this.saveContext();
|
||||
};
|
||||
|
||||
async saveContext() {
|
||||
if (!this.browser_context || !this.page_context) return;
|
||||
|
||||
try {
|
||||
const cookies = await this.browser_context.cookies();
|
||||
const localStorageData = await this.page_context.evaluate(() =>
|
||||
JSON.stringify(localStorage)
|
||||
);
|
||||
|
||||
const contextData = {
|
||||
cookies,
|
||||
localStorage: localStorageData,
|
||||
};
|
||||
|
||||
const dirPath = path.join(CONSTANTS.PROFILE_PATH);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`📂 Save at folder: ${dirPath}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"),
|
||||
JSON.stringify(contextData, null, 2)
|
||||
);
|
||||
console.log("✅ Context saved!");
|
||||
} catch (error) {
|
||||
console.log("Save Context: ", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.children = children;
|
||||
this.origin_url = origin_url;
|
||||
this.active = active;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.url = url;
|
||||
}
|
||||
async restoreContext() {
|
||||
if (!this.browser_context || !this.page_context) return;
|
||||
|
||||
puppeteer_connect = async () => {
|
||||
this.browser_context = await browser.createBrowserContext();
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
const page = await this.browser_context.newPage();
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
|
||||
this.page_context = page;
|
||||
const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
|
||||
await this.restoreContext();
|
||||
};
|
||||
// Restore Cookies
|
||||
await this.page_context.setCookie(...contextData.cookies);
|
||||
|
||||
listen_events = async () => {
|
||||
if (this.page_context) return;
|
||||
console.log("🔄 Context restored!");
|
||||
}
|
||||
|
||||
await this.puppeteer_connect();
|
||||
async onCloseLogin() {}
|
||||
|
||||
await this.action();
|
||||
async isTimeToLogin() {
|
||||
const earlyLoginTime = findEarlyLoginTime(this);
|
||||
|
||||
await this.saveContext();
|
||||
};
|
||||
|
||||
async saveContext() {
|
||||
if (!this.browser_context || !this.page_context) return;
|
||||
|
||||
try {
|
||||
const cookies = await this.browser_context.cookies();
|
||||
const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage));
|
||||
|
||||
const contextData = {
|
||||
cookies,
|
||||
localStorage: localStorageData,
|
||||
};
|
||||
|
||||
const dirPath = path.join(CONSTANTS.PROFILE_PATH);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`📂 Save at folder: ${dirPath}`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(dirPath, sanitizeFileName(this.origin_url) + '.json'), JSON.stringify(contextData, null, 2));
|
||||
console.log('✅ Context saved!');
|
||||
} catch (error) {
|
||||
console.log('Save Context: ', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async restoreContext() {
|
||||
if (!this.browser_context || !this.page_context) return;
|
||||
|
||||
const filePath = getPathProfile(this.origin_url);
|
||||
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
|
||||
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
|
||||
// Restore Cookies
|
||||
await this.page_context.setCookie(...contextData.cookies);
|
||||
|
||||
console.log('🔄 Context restored!');
|
||||
}
|
||||
return earlyLoginTime && isTimeReached(earlyLoginTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ export class LangtonsApiBid extends ApiBid {
|
|||
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
||||
);
|
||||
await safeClosePage(this);
|
||||
|
||||
await this.onCloseLogin(this);
|
||||
}
|
||||
|
||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||
|
|
|
|||
|
|
@ -79,9 +79,21 @@ export class LangtonsProductBid extends ProductBid {
|
|||
timeout / 1000
|
||||
}s`
|
||||
);
|
||||
this.page_context.off("response", onResponse); // Gỡ bỏ listener khi timeout
|
||||
this.page_context?.off("response", onResponse); // Gỡ bỏ listener khi timeout
|
||||
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page
|
||||
try {
|
||||
if (!this.page_context.isClosed()) {
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
|
||||
} else {
|
||||
console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ [${this.id}] Error reloading page:`,
|
||||
error?.message
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
|
||||
resolve(null);
|
||||
|
|
@ -161,8 +173,8 @@ export class LangtonsProductBid extends ProductBid {
|
|||
lot_id: result?.lotId || null,
|
||||
reserve_price: result.lotData?.minimumBid || null,
|
||||
current_price: result.lotData?.currentMaxBid || null,
|
||||
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
||||
close_time: close_time ? String(close_time) : null,
|
||||
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
||||
name,
|
||||
},
|
||||
// [],
|
||||
|
|
@ -441,7 +453,11 @@ export class LangtonsProductBid extends ProductBid {
|
|||
console.log(
|
||||
`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
|
||||
);
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
|
||||
if (!this.page_context.isClosed()) {
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
}
|
||||
|
||||
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ export class LawsonsProductBid extends ProductBid {
|
|||
`===============Start call to submit [${this.id}] ================`
|
||||
);
|
||||
|
||||
await delay(2000);
|
||||
await delay(200);
|
||||
|
||||
// Nếu chưa bid, thực hiện đặt giá
|
||||
console.log(
|
||||
|
|
@ -368,7 +368,10 @@ export class LawsonsProductBid extends ProductBid {
|
|||
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
|
||||
}
|
||||
|
||||
if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
|
||||
if (
|
||||
new Date(this.updated_at).getTime() > Date.now() - 120 * 1000 &&
|
||||
!this.page_context.isClosed()
|
||||
) {
|
||||
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,108 +1,147 @@
|
|||
import CONSTANTS from './constants.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { updateStatusWork } from './apis/bid.js';
|
||||
import CONSTANTS from "./constants.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { updateStatusWork } from "./apis/bid.js";
|
||||
|
||||
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
||||
|
||||
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
|
||||
if (!page || page.isClosed()) return;
|
||||
export const takeSnapshot = async (
|
||||
page,
|
||||
item,
|
||||
imageName,
|
||||
type = CONSTANTS.TYPE_IMAGE.ERRORS
|
||||
) => {
|
||||
if (!page || page.isClosed()) return;
|
||||
|
||||
try {
|
||||
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
|
||||
const typeDir = path.join(baseDir, type); // Thư mục con theo type
|
||||
try {
|
||||
const baseDir = path.join(
|
||||
CONSTANTS.ERROR_IMAGES_PATH,
|
||||
item.type,
|
||||
String(item.id)
|
||||
); // Thư mục theo lot_id
|
||||
const typeDir = path.join(baseDir, type); // Thư mục con theo type
|
||||
|
||||
// Tạo tên file, nếu type === 'work' thì không có timestamp
|
||||
const fileName = type === CONSTANTS.TYPE_IMAGE.WORK ? `${imageName}.png` : `${imageName}_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
||||
// Tạo tên file, nếu type === 'work' thì không có timestamp
|
||||
const fileName =
|
||||
type === CONSTANTS.TYPE_IMAGE.WORK
|
||||
? `${imageName}.png`
|
||||
: `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
|
||||
|
||||
const filePath = path.join(typeDir, fileName);
|
||||
const filePath = path.join(typeDir, fileName);
|
||||
|
||||
// Kiểm tra và tạo thư mục nếu chưa tồn tại
|
||||
if (!fs.existsSync(typeDir)) {
|
||||
fs.mkdirSync(typeDir, { recursive: true });
|
||||
console.log(`📂 Save at folder: ${typeDir}`);
|
||||
}
|
||||
|
||||
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
||||
// Kiểm tra có thể điều hướng trang không
|
||||
const isPageResponsive = await page.evaluate(() => document.readyState === 'complete');
|
||||
if (!isPageResponsive) {
|
||||
console.log('🚫 Page is unresponsive, skipping snapshot.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
|
||||
await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => {
|
||||
console.log('⚠️ Body selector not found, skipping snapshot.');
|
||||
return;
|
||||
});
|
||||
|
||||
// Chụp ảnh màn hình và lưu vào filePath
|
||||
await page.screenshot({ path: filePath });
|
||||
|
||||
console.log(`📸 Image saved at: ${filePath}`);
|
||||
|
||||
// Nếu type === 'work', gửi ảnh lên API
|
||||
if (type === CONSTANTS.TYPE_IMAGE.WORK) {
|
||||
await updateStatusWork(item, filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error when snapshot: ' + error.message);
|
||||
// Kiểm tra và tạo thư mục nếu chưa tồn tại
|
||||
if (!fs.existsSync(typeDir)) {
|
||||
fs.mkdirSync(typeDir, { recursive: true });
|
||||
console.log(`📂 Save at folder: ${typeDir}`);
|
||||
}
|
||||
|
||||
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
|
||||
// Kiểm tra có thể điều hướng trang không
|
||||
const isPageResponsive = await page.evaluate(
|
||||
() => document.readyState === "complete"
|
||||
);
|
||||
if (!isPageResponsive) {
|
||||
console.log("🚫 Page is unresponsive, skipping snapshot.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
|
||||
await page
|
||||
.waitForSelector("body", { visible: true, timeout: 15000 })
|
||||
.catch(() => {
|
||||
console.log("⚠️ Body selector not found, skipping snapshot.");
|
||||
return;
|
||||
});
|
||||
|
||||
// Chụp ảnh màn hình và lưu vào filePath
|
||||
await page.screenshot({ path: filePath });
|
||||
|
||||
console.log(`📸 Image saved at: ${filePath}`);
|
||||
|
||||
// Nếu type === 'work', gửi ảnh lên API
|
||||
if (type === CONSTANTS.TYPE_IMAGE.WORK) {
|
||||
await updateStatusWork(item, filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error when snapshot: " + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function safeClosePage(item) {
|
||||
try {
|
||||
const page = item.page_context;
|
||||
export const safeClosePageReal = async (page) => {
|
||||
if (!page) return;
|
||||
|
||||
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);
|
||||
try {
|
||||
if (page.isClosed()) {
|
||||
console.log(`✅ Page already closed: ${page.url()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close
|
||||
await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn
|
||||
console.log(`✅ Successfully closed page: ${page.url()}`);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export async function safeClosePage(item) {
|
||||
try {
|
||||
const page = item.page_context;
|
||||
|
||||
if (!page?.isClosed() && page?.close) {
|
||||
await safeClosePageReal(page);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
export function isTimeReached(targetTime) {
|
||||
if (!targetTime) return false;
|
||||
if (!targetTime) return false;
|
||||
|
||||
const targetDate = new Date(targetTime);
|
||||
const now = new Date();
|
||||
const targetDate = new Date(targetTime);
|
||||
const now = new Date();
|
||||
|
||||
return now >= targetDate;
|
||||
return now >= targetDate;
|
||||
}
|
||||
|
||||
export function extractNumber(str) {
|
||||
const match = str.match(/\d+(\.\d+)?/);
|
||||
return match ? parseFloat(match[0]) : null;
|
||||
const match = str.match(/\d+(\.\d+)?/);
|
||||
return match ? parseFloat(match[0]) : null;
|
||||
}
|
||||
|
||||
export const sanitizeFileName = (url) => {
|
||||
return url.replace(/[:\/]/g, '_');
|
||||
return url.replace(/[:\/]/g, "_");
|
||||
};
|
||||
|
||||
export const getPathProfile = (origin_url) => {
|
||||
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
|
||||
return path.join(
|
||||
CONSTANTS.PROFILE_PATH,
|
||||
sanitizeFileName(origin_url) + ".json"
|
||||
);
|
||||
};
|
||||
|
||||
export function removeFalsyValues(obj, excludeKeys = []) {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key)) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key)) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export const enableAutoBidMessage = (data) => {
|
||||
return `
|
||||
return `
|
||||
<b>⭉ Activate Auto Bid</b><br>
|
||||
📌 Product: <b>${data.name}</b><br>
|
||||
🔗 Link: <a href="${data.url}">Click here</a><br>
|
||||
|
|
@ -112,69 +151,110 @@ export const enableAutoBidMessage = (data) => {
|
|||
};
|
||||
|
||||
export function convertAETtoUTC(dateString) {
|
||||
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
|
||||
const monthMap = {
|
||||
Jan: 0,
|
||||
Feb: 1,
|
||||
Mar: 2,
|
||||
Apr: 3,
|
||||
May: 4,
|
||||
Jun: 5,
|
||||
Jul: 6,
|
||||
Aug: 7,
|
||||
Sep: 8,
|
||||
Oct: 9,
|
||||
Nov: 10,
|
||||
Dec: 11,
|
||||
};
|
||||
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
|
||||
const monthMap = {
|
||||
Jan: 0,
|
||||
Feb: 1,
|
||||
Mar: 2,
|
||||
Apr: 3,
|
||||
May: 4,
|
||||
Jun: 5,
|
||||
Jul: 6,
|
||||
Aug: 7,
|
||||
Sep: 8,
|
||||
Oct: 9,
|
||||
Nov: 10,
|
||||
Dec: 11,
|
||||
};
|
||||
|
||||
// Tách chuỗi đầu vào
|
||||
const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
|
||||
if (!parts) {
|
||||
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
|
||||
}
|
||||
// Tách chuỗi đầu vào
|
||||
const parts = dateString.match(
|
||||
/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
|
||||
);
|
||||
if (!parts) {
|
||||
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
|
||||
}
|
||||
|
||||
const [, , day, month, year, hour, period] = parts;
|
||||
const [, , day, month, year, hour, period] = parts;
|
||||
|
||||
// Chuyển đổi giờ sang định dạng 24h
|
||||
let hours = parseInt(hour, 10);
|
||||
if (period === 'PM' && hours !== 12) hours += 12;
|
||||
if (period === 'AM' && hours === 12) hours = 0;
|
||||
// Chuyển đổi giờ sang định dạng 24h
|
||||
let hours = parseInt(hour, 10);
|
||||
if (period === "PM" && hours !== 12) hours += 12;
|
||||
if (period === "AM" && hours === 12) hours = 0;
|
||||
|
||||
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
|
||||
const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0));
|
||||
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
|
||||
const date = new Date(
|
||||
Date.UTC(
|
||||
parseInt(year, 10),
|
||||
monthMap[month],
|
||||
parseInt(day, 10),
|
||||
hours,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
// Hàm kiểm tra DST cho AET
|
||||
function isDST(date) {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth();
|
||||
const day = date.getUTCDate();
|
||||
// Hàm kiểm tra DST cho AET
|
||||
function isDST(date) {
|
||||
const year = date.getUTCFullYear();
|
||||
const month = date.getUTCMonth();
|
||||
const day = date.getUTCDate();
|
||||
|
||||
// DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
|
||||
const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
|
||||
dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
||||
const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
|
||||
// DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
|
||||
const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
|
||||
dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
||||
const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
|
||||
|
||||
// DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
|
||||
const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
|
||||
dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
||||
const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
|
||||
// DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
|
||||
const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
|
||||
dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
|
||||
const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
|
||||
|
||||
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
|
||||
return currentTime >= dstStartTime && currentTime < dstEndTime;
|
||||
}
|
||||
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
|
||||
return currentTime >= dstStartTime && currentTime < dstEndTime;
|
||||
}
|
||||
|
||||
// Xác định offset dựa trên DST
|
||||
const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
|
||||
// Xác định offset dựa trên DST
|
||||
const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
|
||||
|
||||
// Điều chỉnh thời gian về UTC
|
||||
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
|
||||
// Điều chỉnh thời gian về UTC
|
||||
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
|
||||
|
||||
// Trả về chuỗi UTC
|
||||
return utcDate.toUTCString();
|
||||
// Trả về chuỗi UTC
|
||||
return utcDate.toUTCString();
|
||||
}
|
||||
|
||||
export function extractPriceNumber(priceString) {
|
||||
const cleaned = priceString.replace(/[^\d.]/g, '');
|
||||
return parseFloat(cleaned);
|
||||
const cleaned = priceString.replace(/[^\d.]/g, "");
|
||||
return parseFloat(cleaned);
|
||||
}
|
||||
|
||||
export function findEarlyLoginTime(webBid) {
|
||||
const now = new Date();
|
||||
|
||||
// Bước 1: Lọc ra những bid có close_time hợp lệ
|
||||
const validChildren = webBid.children.filter((child) => child.close_time);
|
||||
|
||||
if (validChildren.length === 0) return null;
|
||||
|
||||
// Bước 2: Tìm bid có close_time gần hiện tại nhất
|
||||
const closestBid = validChildren.reduce((closest, current) => {
|
||||
const closestDiff = Math.abs(
|
||||
new Date(closest.close_time).getTime() - now.getTime()
|
||||
);
|
||||
const currentDiff = Math.abs(
|
||||
new Date(current.close_time).getTime() - now.getTime()
|
||||
);
|
||||
return currentDiff < closestDiff ? current : closest;
|
||||
});
|
||||
|
||||
if (!closestBid.close_time) return null;
|
||||
|
||||
// Bước 3: Tính toán thời gian login sớm
|
||||
const closeTime = new Date(closestBid.close_time);
|
||||
closeTime.setSeconds(
|
||||
closeTime.getSeconds() - (webBid.early_login_seconds || 0)
|
||||
);
|
||||
|
||||
return closeTime.toISOString();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue