pickxel and fix login
This commit is contained in:
parent
b13712a317
commit
65ae7da6e6
|
|
@ -1,92 +1,110 @@
|
||||||
import { generateNestParams, handleError, handleSuccess } from '.';
|
import { generateNestParams, handleError, handleSuccess } from ".";
|
||||||
import axios from '../lib/axios';
|
import axios from "../lib/axios";
|
||||||
import { IWebBid } from '../system/type';
|
import { IWebBid } from "../system/type";
|
||||||
import { removeFalsyValues } from '../utils';
|
import { removeFalsyValues } from "../utils";
|
||||||
|
|
||||||
const BASE_URL = 'web-bids';
|
const BASE_URL = "web-bids";
|
||||||
|
|
||||||
export const getWebBids = async (params: Record<string, string | number>) => {
|
export const getWebBids = async (params: Record<string, string | number>) => {
|
||||||
return await axios({
|
return await axios({
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
params: generateNestParams(params),
|
params: generateNestParams(params),
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
|
export const createWebBid = async (
|
||||||
const newData = removeFalsyValues(bid);
|
bid: Omit<IWebBid, "id" | "created_at" | "updated_at" | "is_system_account">
|
||||||
|
) => {
|
||||||
|
const newData = removeFalsyValues(bid);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
data: newData,
|
data: newData,
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSuccess(data);
|
handleSuccess(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateWebBid = async (bid: Partial<IWebBid>) => {
|
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 {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: `${BASE_URL}/` + bid.id,
|
url: `${BASE_URL}/` + bid.id,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'PUT',
|
method: "PUT",
|
||||||
data: { url, password, username, origin_url, active },
|
data: {
|
||||||
});
|
url,
|
||||||
|
password,
|
||||||
|
username,
|
||||||
|
origin_url,
|
||||||
|
active,
|
||||||
|
arrival_offset_seconds,
|
||||||
|
// early_login_seconds
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
handleSuccess(data);
|
handleSuccess(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteWebBid = async (web: IWebBid) => {
|
export const deleteWebBid = async (web: IWebBid) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: `${BASE_URL}/` + web.id,
|
url: `${BASE_URL}/` + web.id,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'DELETE',
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSuccess(data);
|
handleSuccess(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deletesWebBid = async (web: IWebBid[]) => {
|
export const deletesWebBid = async (web: IWebBid[]) => {
|
||||||
const ids = web.reduce((prev, cur) => {
|
const ids = web.reduce((prev, cur) => {
|
||||||
prev.push(cur.id);
|
prev.push(cur.id);
|
||||||
return prev;
|
return prev;
|
||||||
}, [] as number[]);
|
}, [] as number[]);
|
||||||
try {
|
try {
|
||||||
const { data } = await axios({
|
const { data } = await axios({
|
||||||
url: `${BASE_URL}/deletes`,
|
url: `${BASE_URL}/deletes`,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
ids,
|
ids,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
handleSuccess(data);
|
handleSuccess(data);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...pr
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>{element['bidderAnonName']}</Table.Td>
|
<Table.Td>{element['bidderAnonName']}</Table.Td>
|
||||||
<Table.Td>{element['actualBid']}</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())}</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>
|
</Table.Tr>
|
||||||
));
|
));
|
||||||
}, [histories]);
|
}, [histories]);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH
|
||||||
<Table.Tr key={element.id}>
|
<Table.Tr key={element.id}>
|
||||||
<Table.Td>{element.id}</Table.Td>
|
<Table.Td>{element.id}</Table.Td>
|
||||||
<Table.Td>{element.price}</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>
|
</Table.Tr>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,196 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core';
|
import {
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
Button,
|
||||||
import _ from 'lodash';
|
LoadingOverlay,
|
||||||
import { useEffect, useRef, useState } from 'react';
|
Modal,
|
||||||
import { z } from 'zod';
|
ModalProps,
|
||||||
import { createWebBid, updateWebBid } from '../../apis/web-bid';
|
NumberInput,
|
||||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
|
TextInput,
|
||||||
import { IWebBid } from '../../system/type';
|
} from "@mantine/core";
|
||||||
import { extractDomain } from '../../utils';
|
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 {
|
export interface IWebBidModelProps extends ModalProps {
|
||||||
data: IWebBid | null;
|
data: IWebBid | null;
|
||||||
onUpdated?: () => void;
|
onUpdated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
|
url: z.string({ message: "Url is required" }).url("Invalid url format"),
|
||||||
|
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) {
|
export default function WebBidModal({
|
||||||
const form = useForm({
|
data,
|
||||||
validate: zodResolver(z.object(schema)),
|
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) => {
|
const handleSubmit = async (values: typeof form.values) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
setConfirm({
|
setConfirm({
|
||||||
title: 'Update ?',
|
title: "Update ?",
|
||||||
message: `This web will be update`,
|
message: `This web will be update`,
|
||||||
handleOk: async () => {
|
handleOk: async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateWebBid(values);
|
console.log(
|
||||||
setLoading(false);
|
"%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) {
|
if (onUpdated) {
|
||||||
onUpdated();
|
onUpdated();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
okButton: {
|
okButton: {
|
||||||
color: 'blue',
|
color: "blue",
|
||||||
value: 'Update',
|
value: "Update",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const { url, origin_url } = values;
|
const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await createWebBid({ url, origin_url } as IWebBid);
|
const result = await createWebBid({
|
||||||
setLoading(false);
|
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) {
|
if (onUpdated) {
|
||||||
onUpdated();
|
onUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
form.setValues(data);
|
form.setValues(data);
|
||||||
|
|
||||||
prevData.current = data;
|
prevData.current = data;
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.opened) {
|
if (!props.opened) {
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.opened]);
|
}, [props.opened]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.values?.url) {
|
if (form.values?.url) {
|
||||||
form.setFieldValue('origin_url', extractDomain(form.values.url));
|
form.setFieldValue("origin_url", extractDomain(form.values.url));
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [form.values]);
|
}, [form.values]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="relative"
|
className="relative"
|
||||||
classNames={{
|
classNames={{
|
||||||
header: '!flex !item-center !justify-center w-full',
|
header: "!flex !item-center !justify-center w-full",
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
size={'xl'}
|
size={"xl"}
|
||||||
title={<span className="text-xl font-bold">Web</span>}
|
title={<span className="text-xl font-bold">Web</span>}
|
||||||
centered
|
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">
|
{data ? "Update" : "Create"}
|
||||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
|
</Button>
|
||||||
<TextInput withAsterisk className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
|
</form>
|
||||||
|
|
||||||
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
|
<LoadingOverlay
|
||||||
{data ? 'Update' : 'Create'}
|
visible={loading}
|
||||||
</Button>
|
zIndex={1000}
|
||||||
</form>
|
overlayProps={{ blur: 2 }}
|
||||||
|
/>
|
||||||
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
|
</Modal>
|
||||||
</Modal>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,7 @@ export interface ITimestamp {
|
||||||
updated_at: string;
|
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 {
|
export interface IHistory extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp {
|
||||||
username: string | null;
|
username: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
arrival_offset_seconds: number;
|
||||||
|
early_login_seconds: number;
|
||||||
children: IBid[];
|
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 {
|
export interface IPermission extends ITimestamp {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
import { IWebBid } from "../system/type";
|
||||||
|
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||||
export function cn(...args: ClassValue[]) {
|
export function cn(...args: ClassValue[]) {
|
||||||
return twMerge(clsx(args));
|
return twMerge(clsx(args));
|
||||||
}
|
}
|
||||||
|
|
@ -150,4 +154,31 @@ export function stringToColor(str: string): string {
|
||||||
const hash = hashStringToInt(str);
|
const hash = hashStringToInt(str);
|
||||||
const index = hash % colorPalette.length;
|
const index = hash % colorPalette.length;
|
||||||
return colorPalette[index];
|
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){
|
switch(bid.web_bid.origin_url){
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// GRAYS
|
||||||
|
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||||
case 'https://www.grays.com': {
|
case 'https://www.grays.com': {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`,
|
||||||
|
|
@ -32,6 +36,11 @@ export class GraysApi {
|
||||||
|
|
||||||
return AppResponse.toResponse([])
|
return AppResponse.toResponse([])
|
||||||
}
|
}
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
|
||||||
|
// PICKLES
|
||||||
|
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||||
case 'https://www.pickles.com.au': {
|
case 'https://www.pickles.com.au': {
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
|
|
@ -39,7 +48,11 @@ export class GraysApi {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
|
<<<<<<< HEAD
|
||||||
return AppResponse.toResponse(response.data);
|
return AppResponse.toResponse(response.data);
|
||||||
|
=======
|
||||||
|
return AppResponse.toResponse(response.data.Bids);
|
||||||
|
>>>>>>> 26b10a7 (pickxel and fix login)
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppResponse.toResponse([])
|
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 {
|
export class UpdateWebBidDto {
|
||||||
@IsUrl()
|
@IsUrl()
|
||||||
|
|
@ -9,6 +9,16 @@ export class UpdateWebBidDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
url: string;
|
url: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Min(60)
|
||||||
|
@IsOptional()
|
||||||
|
arrival_offset_seconds: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Min(600)
|
||||||
|
@IsOptional()
|
||||||
|
early_login_seconds: number;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
username: string;
|
username: string;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,12 @@ export class WebBid extends Timestamp {
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
@Column({ default: 300 })
|
||||||
|
arrival_offset_seconds: number;
|
||||||
|
|
||||||
|
@Column({ default: 600 })
|
||||||
|
early_login_seconds: number;
|
||||||
|
|
||||||
@Column({ default: null, nullable: true })
|
@Column({ default: null, nullable: true })
|
||||||
@Exclude()
|
@Exclude()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ export class BidsService {
|
||||||
if (!bid.close_time && !bid.start_bid_time) {
|
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
|
// 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 = 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)
|
// 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']) {
|
async emitAccountUpdate(id: WebBid['id']) {
|
||||||
const data = await this.webBidRepo.findOne({
|
const data = await this.webBidRepo.findOne({
|
||||||
where: { id, children: { status: 'biding' } },
|
where: { id, children: { status: 'biding' } },
|
||||||
relations: { children: true },
|
relations: { children: { web_bid: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.emit(Event.WEB_UPDATED, data || null);
|
this.eventEmitter.emit(Event.WEB_UPDATED, data || null);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ import {
|
||||||
} from "./service/app-service.js";
|
} from "./service/app-service.js";
|
||||||
import browser from "./system/browser.js";
|
import browser from "./system/browser.js";
|
||||||
import configs from "./system/config.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";
|
import { updateLoginStatus } from "./system/apis/bid.js";
|
||||||
|
|
||||||
global.IS_CLEANING = true;
|
global.IS_CLEANING = true;
|
||||||
|
|
@ -53,6 +58,59 @@ const handleUpdateProductTabs = (data) => {
|
||||||
MANAGER_BIDS = newDataManager;
|
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 () => {
|
const tracking = async () => {
|
||||||
console.log("🚀 Tracking process started...");
|
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(
|
Promise.allSettled(
|
||||||
productTabs.map(async (productTab) => {
|
productTabs.map(async (productTab) => {
|
||||||
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
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 () => {
|
const clearLazyTab = async () => {
|
||||||
if (!global.IS_CLEANING) {
|
if (!global.IS_CLEANING) {
|
||||||
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
||||||
|
|
@ -172,47 +310,53 @@ const clearLazyTab = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = await browser.pages();
|
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) => [
|
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
||||||
item.url,
|
item.url,
|
||||||
...item.children.map((child) => child.url),
|
...item.children.map((child) => child.url),
|
||||||
]).filter(Boolean); // Lọc bỏ null hoặc undefined
|
]).filter(Boolean);
|
||||||
|
|
||||||
console.log(
|
|
||||||
"🔍 Page URLs:",
|
|
||||||
pages.map((page) => page.url())
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const page of pages) {
|
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
|
const pageUrl = page.url();
|
||||||
if (!pageUrl || pageUrl === "about:blank") continue;
|
|
||||||
|
|
||||||
if (!activeUrls.includes(pageUrl)) {
|
if (!pageUrl || pageUrl === "about:blank") continue;
|
||||||
if (!page.isClosed() && browser.isConnected()) {
|
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 {
|
try {
|
||||||
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
await Promise.race([
|
||||||
.map((i) => ({
|
page.close(),
|
||||||
current_url: i.page_context.url(),
|
new Promise((_, reject) =>
|
||||||
data: i,
|
setTimeout(() => reject(new Error("Close timeout")), 3000)
|
||||||
}))
|
),
|
||||||
.find((j) => j.current_url === pageUrl);
|
]);
|
||||||
|
} catch (closeErr) {
|
||||||
console.log(bidData);
|
console.warn(
|
||||||
|
`⚠️ Error closing page ${pageUrl}: ${closeErr.message}`
|
||||||
if (bidData && bidData.data) {
|
);
|
||||||
await safeClosePage(bidData.data);
|
|
||||||
} else {
|
|
||||||
await page.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🛑 Closing unused tab: ${pageUrl}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Closed page: ${pageUrl}`);
|
||||||
|
} catch (pageErr) {
|
||||||
|
console.warn(`⚠️ Error handling page: ${pageErr.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -343,6 +487,10 @@ const trackingLoginStatus = async () => {
|
||||||
|
|
||||||
await safeClosePage(tab);
|
await safeClosePage(tab);
|
||||||
|
|
||||||
|
MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id);
|
||||||
|
|
||||||
|
addProductTab(data);
|
||||||
|
|
||||||
global.IS_CLEANING = true;
|
global.IS_CLEANING = true;
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ No profile found to delete.");
|
console.log("⚠️ No profile found to delete.");
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,144 @@
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
import BID_TYPE from '../system/bid-type.js';
|
import BID_TYPE from "../system/bid-type.js";
|
||||||
import browser from '../system/browser.js';
|
import browser from "../system/browser.js";
|
||||||
import CONSTANTS from '../system/constants.js';
|
import CONSTANTS from "../system/constants.js";
|
||||||
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
|
import {
|
||||||
import { Bid } from './bid.js';
|
findEarlyLoginTime,
|
||||||
|
getPathProfile,
|
||||||
|
isTimeReached,
|
||||||
|
sanitizeFileName,
|
||||||
|
} from "../system/utils.js";
|
||||||
|
import { Bid } from "./bid.js";
|
||||||
|
|
||||||
export class ApiBid extends Bid {
|
export class ApiBid extends Bid {
|
||||||
id;
|
id;
|
||||||
account;
|
account;
|
||||||
children = [];
|
children = [];
|
||||||
children_processing = [];
|
children_processing = [];
|
||||||
created_at;
|
created_at;
|
||||||
updated_at;
|
updated_at;
|
||||||
origin_url;
|
origin_url;
|
||||||
active;
|
active;
|
||||||
browser_context;
|
browser_context;
|
||||||
username;
|
username;
|
||||||
password;
|
password;
|
||||||
|
|
||||||
constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
|
constructor({
|
||||||
super(BID_TYPE.API_BID, url);
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
origin_url,
|
||||||
|
active,
|
||||||
|
}) {
|
||||||
|
super(BID_TYPE.API_BID, url);
|
||||||
|
|
||||||
this.created_at = created_at;
|
this.created_at = created_at;
|
||||||
this.updated_at = updated_at;
|
this.updated_at = updated_at;
|
||||||
this.children = children;
|
this.children = children;
|
||||||
this.origin_url = origin_url;
|
this.origin_url = origin_url;
|
||||||
this.active = active;
|
this.active = active;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.id = id;
|
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 }) {
|
async restoreContext() {
|
||||||
this.created_at = created_at;
|
if (!this.browser_context || !this.page_context) return;
|
||||||
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 () => {
|
const filePath = getPathProfile(this.origin_url);
|
||||||
this.browser_context = await browser.createBrowserContext();
|
|
||||||
|
|
||||||
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 () => {
|
console.log("🔄 Context restored!");
|
||||||
if (this.page_context) return;
|
}
|
||||||
|
|
||||||
await this.puppeteer_connect();
|
async onCloseLogin() {}
|
||||||
|
|
||||||
await this.action();
|
async isTimeToLogin() {
|
||||||
|
const earlyLoginTime = findEarlyLoginTime(this);
|
||||||
|
|
||||||
await this.saveContext();
|
return earlyLoginTime && isTimeReached(earlyLoginTime);
|
||||||
};
|
}
|
||||||
|
|
||||||
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!');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ export class LangtonsApiBid extends ApiBid {
|
||||||
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
`➡ [${this.id}] Closing main page context: ${this.page_context}`
|
||||||
);
|
);
|
||||||
await safeClosePage(this);
|
await safeClosePage(this);
|
||||||
|
|
||||||
|
await this.onCloseLogin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔑 [${this.id}] Starting login process...`);
|
console.log(`🔑 [${this.id}] Starting login process...`);
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,21 @@ export class LangtonsProductBid extends ProductBid {
|
||||||
timeout / 1000
|
timeout / 1000
|
||||||
}s`
|
}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`);
|
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
|
|
@ -161,8 +173,8 @@ export class LangtonsProductBid extends ProductBid {
|
||||||
lot_id: result?.lotId || null,
|
lot_id: result?.lotId || null,
|
||||||
reserve_price: result.lotData?.minimumBid || null,
|
reserve_price: result.lotData?.minimumBid || null,
|
||||||
current_price: result.lotData?.currentMaxBid || 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 ? String(close_time) : null,
|
||||||
|
// close_time: close_time && !this.close_time ? String(close_time) : null,
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
// [],
|
// [],
|
||||||
|
|
@ -441,7 +453,11 @@ export class LangtonsProductBid extends ProductBid {
|
||||||
console.log(
|
console.log(
|
||||||
`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
|
`⚠️ [${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`);
|
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ export class LawsonsProductBid extends ProductBid {
|
||||||
`===============Start call to submit [${this.id}] ================`
|
`===============Start call to submit [${this.id}] ================`
|
||||||
);
|
);
|
||||||
|
|
||||||
await delay(2000);
|
await delay(200);
|
||||||
|
|
||||||
// Nếu chưa bid, thực hiện đặt giá
|
// Nếu chưa bid, thực hiện đặt giá
|
||||||
console.log(
|
console.log(
|
||||||
|
|
@ -368,7 +368,10 @@ export class LawsonsProductBid extends ProductBid {
|
||||||
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
|
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" });
|
await this.page_context.reload({ waitUntil: "networkidle0" });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,147 @@
|
||||||
import CONSTANTS from './constants.js';
|
import CONSTANTS from "./constants.js";
|
||||||
import fs from 'fs';
|
import fs from "fs";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
import { updateStatusWork } from './apis/bid.js';
|
import { updateStatusWork } from "./apis/bid.js";
|
||||||
|
|
||||||
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
|
||||||
|
|
||||||
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
|
export const takeSnapshot = async (
|
||||||
if (!page || page.isClosed()) return;
|
page,
|
||||||
|
item,
|
||||||
|
imageName,
|
||||||
|
type = CONSTANTS.TYPE_IMAGE.ERRORS
|
||||||
|
) => {
|
||||||
|
if (!page || page.isClosed()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
|
const baseDir = path.join(
|
||||||
const typeDir = path.join(baseDir, type); // Thư mục con theo type
|
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
|
// 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 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
|
// Kiểm tra và tạo thư mục nếu chưa tồn tại
|
||||||
if (!fs.existsSync(typeDir)) {
|
if (!fs.existsSync(typeDir)) {
|
||||||
fs.mkdirSync(typeDir, { recursive: true });
|
fs.mkdirSync(typeDir, { recursive: true });
|
||||||
console.log(`📂 Save at folder: ${typeDir}`);
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export async function safeClosePage(item) {
|
export const safeClosePageReal = async (page) => {
|
||||||
try {
|
if (!page) return;
|
||||||
const page = item.page_context;
|
|
||||||
|
|
||||||
if (!page?.isClosed() && page?.close) {
|
try {
|
||||||
await page.close();
|
if (page.isClosed()) {
|
||||||
}
|
console.log(`✅ Page already closed: ${page.url()}`);
|
||||||
|
return;
|
||||||
item.page_context = undefined;
|
|
||||||
if (item?.page_context) {
|
|
||||||
item.page_context = undefined;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Can't close item: " + item.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export function isTimeReached(targetTime) {
|
||||||
if (!targetTime) return false;
|
if (!targetTime) return false;
|
||||||
|
|
||||||
const targetDate = new Date(targetTime);
|
const targetDate = new Date(targetTime);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
return now >= targetDate;
|
return now >= targetDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractNumber(str) {
|
export function extractNumber(str) {
|
||||||
const match = str.match(/\d+(\.\d+)?/);
|
const match = str.match(/\d+(\.\d+)?/);
|
||||||
return match ? parseFloat(match[0]) : null;
|
return match ? parseFloat(match[0]) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sanitizeFileName = (url) => {
|
export const sanitizeFileName = (url) => {
|
||||||
return url.replace(/[:\/]/g, '_');
|
return url.replace(/[:\/]/g, "_");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPathProfile = (origin_url) => {
|
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 = []) {
|
export function removeFalsyValues(obj, excludeKeys = []) {
|
||||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||||
if (value || excludeKeys.includes(key)) {
|
if (value || excludeKeys.includes(key)) {
|
||||||
acc[key] = value;
|
acc[key] = value;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enableAutoBidMessage = (data) => {
|
export const enableAutoBidMessage = (data) => {
|
||||||
return `
|
return `
|
||||||
<b>⭉ Activate Auto Bid</b><br>
|
<b>⭉ Activate Auto Bid</b><br>
|
||||||
📌 Product: <b>${data.name}</b><br>
|
📌 Product: <b>${data.name}</b><br>
|
||||||
🔗 Link: <a href="${data.url}">Click here</a><br>
|
🔗 Link: <a href="${data.url}">Click here</a><br>
|
||||||
|
|
@ -112,69 +151,110 @@ export const enableAutoBidMessage = (data) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function convertAETtoUTC(dateString) {
|
export function convertAETtoUTC(dateString) {
|
||||||
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
|
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
|
||||||
const monthMap = {
|
const monthMap = {
|
||||||
Jan: 0,
|
Jan: 0,
|
||||||
Feb: 1,
|
Feb: 1,
|
||||||
Mar: 2,
|
Mar: 2,
|
||||||
Apr: 3,
|
Apr: 3,
|
||||||
May: 4,
|
May: 4,
|
||||||
Jun: 5,
|
Jun: 5,
|
||||||
Jul: 6,
|
Jul: 6,
|
||||||
Aug: 7,
|
Aug: 7,
|
||||||
Sep: 8,
|
Sep: 8,
|
||||||
Oct: 9,
|
Oct: 9,
|
||||||
Nov: 10,
|
Nov: 10,
|
||||||
Dec: 11,
|
Dec: 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tách chuỗi đầu vào
|
// Tách chuỗi đầu vào
|
||||||
const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
|
const parts = dateString.match(
|
||||||
if (!parts) {
|
/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
|
||||||
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
|
);
|
||||||
}
|
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
|
// Chuyển đổi giờ sang định dạng 24h
|
||||||
let hours = parseInt(hour, 10);
|
let hours = parseInt(hour, 10);
|
||||||
if (period === 'PM' && hours !== 12) hours += 12;
|
if (period === "PM" && hours !== 12) hours += 12;
|
||||||
if (period === 'AM' && hours === 12) hours = 0;
|
if (period === "AM" && hours === 12) hours = 0;
|
||||||
|
|
||||||
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
|
// 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));
|
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
|
// Hàm kiểm tra DST cho AET
|
||||||
function isDST(date) {
|
function isDST(date) {
|
||||||
const year = date.getUTCFullYear();
|
const year = date.getUTCFullYear();
|
||||||
const month = date.getUTCMonth();
|
const month = date.getUTCMonth();
|
||||||
const day = date.getUTCDate();
|
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)
|
// 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
|
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
|
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
|
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)
|
// 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
|
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
|
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 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)
|
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
|
||||||
return currentTime >= dstStartTime && currentTime < dstEndTime;
|
return currentTime >= dstStartTime && currentTime < dstEndTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xác định offset dựa trên DST
|
// 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
|
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
|
// Điều chỉnh thời gian về UTC
|
||||||
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
|
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
|
||||||
|
|
||||||
// Trả về chuỗi UTC
|
// Trả về chuỗi UTC
|
||||||
return utcDate.toUTCString();
|
return utcDate.toUTCString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractPriceNumber(priceString) {
|
export function extractPriceNumber(priceString) {
|
||||||
const cleaned = priceString.replace(/[^\d.]/g, '');
|
const cleaned = priceString.replace(/[^\d.]/g, "");
|
||||||
return parseFloat(cleaned);
|
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