Deploy to staging #45
			
				
			
		
		
		
	| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					import { handleError, handleSuccess } from ".";
 | 
				
			||||||
 | 
					import axios from "../lib/axios";
 | 
				
			||||||
 | 
					import { IScrapConfig, IWebBid } from "../system/type";
 | 
				
			||||||
 | 
					import { removeFalsyValues } from "../utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createScrapConfig = async (
 | 
				
			||||||
 | 
					  bid: Omit<
 | 
				
			||||||
 | 
					    IScrapConfig,
 | 
				
			||||||
 | 
					    "id" | "created_at" | "updated_at" | "scrap_items"
 | 
				
			||||||
 | 
					  > & { web_id: IWebBid["id"] }
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const newData = removeFalsyValues(bid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: "scrap-configs",
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      data: newData,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    handleError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const updateScrapConfig = async (scrapConfig: Partial<IScrapConfig>) => {
 | 
				
			||||||
 | 
					  const { search_url, keywords, id } = removeFalsyValues(scrapConfig);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const { data } = await axios({
 | 
				
			||||||
 | 
					      url: "scrap-configs/" + id,
 | 
				
			||||||
 | 
					      withCredentials: true,
 | 
				
			||||||
 | 
					      method: "PUT",
 | 
				
			||||||
 | 
					      data: { search_url, keywords },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleSuccess(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    handleError(error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -217,26 +217,32 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
 | 
					        <Box className="absolute top-0 left-0 py-2  px-4 flex items-center gap-2 justify-between w-full">
 | 
				
			||||||
          <Badge
 | 
					          <Box className="flex items-center gap-2">
 | 
				
			||||||
            color={payloadLoginStatus?.login_status ? "green" : "red"}
 | 
					            <Badge
 | 
				
			||||||
            size="xs"
 | 
					              color={payloadLoginStatus?.login_status ? "green" : "red"}
 | 
				
			||||||
          >
 | 
					              size="xs"
 | 
				
			||||||
            {statusLabel()}
 | 
					            >
 | 
				
			||||||
          </Badge>
 | 
					              {statusLabel()}
 | 
				
			||||||
 | 
					            </Badge>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Badge
 | 
					            <Badge
 | 
				
			||||||
            color={stringToColor(
 | 
					              color={stringToColor(
 | 
				
			||||||
              isIBid(data)
 | 
					                isIBid(data)
 | 
				
			||||||
 | 
					                  ? extractDomainSmart(data.web_bid.origin_url)
 | 
				
			||||||
 | 
					                  : extractDomainSmart(data.origin_url)
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              size="xs"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isIBid(data)
 | 
				
			||||||
                ? extractDomainSmart(data.web_bid.origin_url)
 | 
					                ? extractDomainSmart(data.web_bid.origin_url)
 | 
				
			||||||
                : extractDomainSmart(data.origin_url)
 | 
					                : extractDomainSmart(data.origin_url)}
 | 
				
			||||||
            )}
 | 
					            </Badge>
 | 
				
			||||||
            size="xs"
 | 
					          </Box>
 | 
				
			||||||
          >
 | 
					
 | 
				
			||||||
            {isIBid(data)
 | 
					          {isIBid(data) && moment(data.close_time).isSame(moment(), "day") && (
 | 
				
			||||||
              ? extractDomainSmart(data.web_bid.origin_url)
 | 
					            <div className="w-[14px] h-[14px] rounded-full bg-green-600 animate-pulse"></div>
 | 
				
			||||||
              : extractDomainSmart(data.origin_url)}
 | 
					          )}
 | 
				
			||||||
          </Badge>
 | 
					 | 
				
			||||||
        </Box>
 | 
					        </Box>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,163 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  LoadingOverlay,
 | 
				
			||||||
 | 
					  Modal,
 | 
				
			||||||
 | 
					  ModalProps,
 | 
				
			||||||
 | 
					  Textarea,
 | 
				
			||||||
 | 
					  TextInput,
 | 
				
			||||||
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { createScrapConfig, updateScrapConfig } from "../../apis/scrap";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IScrapConfig, IWebBid } from "../../system/type";
 | 
				
			||||||
 | 
					export interface IScrapConfigModelProps extends ModalProps {
 | 
				
			||||||
 | 
					  data: IWebBid | null;
 | 
				
			||||||
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schema = z.object({
 | 
				
			||||||
 | 
					  search_url: z
 | 
				
			||||||
 | 
					    .string()
 | 
				
			||||||
 | 
					    .url({ message: "Url is invalid" })
 | 
				
			||||||
 | 
					    .min(1, { message: "Url is required" }),
 | 
				
			||||||
 | 
					  keywords: z
 | 
				
			||||||
 | 
					    .string({ message: "Keyword is required" })
 | 
				
			||||||
 | 
					    .min(1, { message: "Keyword is required" })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ScrapConfigModal({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  onUpdated,
 | 
				
			||||||
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IScrapConfigModelProps) {
 | 
				
			||||||
 | 
					  const form = useForm<IScrapConfig>({
 | 
				
			||||||
 | 
					    validate: zodResolver(schema),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loading, setLoading] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const prevData = useRef<IScrapConfig | null>(data?.scrap_config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = async (values: typeof form.values) => {
 | 
				
			||||||
 | 
					    if (data?.scrap_config) {
 | 
				
			||||||
 | 
					      setConfirm({
 | 
				
			||||||
 | 
					        title: "Update ?",
 | 
				
			||||||
 | 
					        message: `This config will be update`,
 | 
				
			||||||
 | 
					        handleOk: async () => {
 | 
				
			||||||
 | 
					          setLoading(true);
 | 
				
			||||||
 | 
					          const result = await updateScrapConfig({
 | 
				
			||||||
 | 
					            ...values,
 | 
				
			||||||
 | 
					            id: data.scrap_config.id,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (onUpdated) {
 | 
				
			||||||
 | 
					            onUpdated();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        okButton: {
 | 
				
			||||||
 | 
					          color: "blue",
 | 
				
			||||||
 | 
					          value: "Update",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      setLoading(true);
 | 
				
			||||||
 | 
					      const result = await createScrapConfig({
 | 
				
			||||||
 | 
					        ...values,
 | 
				
			||||||
 | 
					        web_id: data?.id || 0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!result) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      props.onClose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (onUpdated) {
 | 
				
			||||||
 | 
					        onUpdated();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    form.reset();
 | 
				
			||||||
 | 
					    if (!data) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    form.setValues(data.scrap_config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prevData.current = data.scrap_config;
 | 
				
			||||||
 | 
					    // 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]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Modal
 | 
				
			||||||
 | 
					      className="relative"
 | 
				
			||||||
 | 
					      classNames={{
 | 
				
			||||||
 | 
					        header: "!flex !item-center !justify-center w-full",
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      {...props}
 | 
				
			||||||
 | 
					      size={"xl"}
 | 
				
			||||||
 | 
					      title={<span className="text-xl font-bold">Scrap config</span>}
 | 
				
			||||||
 | 
					      centered
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <form
 | 
				
			||||||
 | 
					        onSubmit={form.onSubmit(handleSubmit)}
 | 
				
			||||||
 | 
					        className="grid grid-cols-2 gap-2.5"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <TextInput
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Search url"
 | 
				
			||||||
 | 
					          withAsterisk
 | 
				
			||||||
 | 
					          description="Replace query keyword in url with phrase {{keyword}}"
 | 
				
			||||||
 | 
					          placeholder="https://www.abc.com/search?q={{keyword}}"
 | 
				
			||||||
 | 
					          {...form.getInputProps("search_url")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Textarea
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Keywords"
 | 
				
			||||||
 | 
					          rows={4}
 | 
				
			||||||
 | 
					          placeholder="msg: Cisco"
 | 
				
			||||||
 | 
					          description={"Different keywords must be separated by commas."}
 | 
				
			||||||
 | 
					          {...form.getInputProps("keywords")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          disabled={_.isEqual(form.getValues(), prevData.current)}
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          type="submit"
 | 
				
			||||||
 | 
					          fullWidth
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          mt="md"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {data?.scrap_config ? "Update" : "Create"}
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <LoadingOverlay
 | 
				
			||||||
 | 
					        visible={loading}
 | 
				
			||||||
 | 
					        zIndex={1000}
 | 
				
			||||||
 | 
					        overlayProps={{ blur: 2 }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Modal>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,109 +1,148 @@
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
import { Button, LoadingOverlay, Modal, ModalProps, PasswordInput, 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 { updateWebBid } from '../../apis/web-bid';
 | 
					  PasswordInput,
 | 
				
			||||||
import { useConfirmStore } from '../../lib/zustand/use-confirm';
 | 
					  TextInput,
 | 
				
			||||||
import { IWebBid } from '../../system/type';
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useForm, zodResolver } from "@mantine/form";
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { useEffect, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					import { updateWebBid } from "../../apis/web-bid";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IWebBid } from "../../system/type";
 | 
				
			||||||
export interface IWebBidModelProps extends ModalProps {
 | 
					export interface IWebBidModelProps extends ModalProps {
 | 
				
			||||||
    data: IWebBid | null;
 | 
					  data: IWebBid | null;
 | 
				
			||||||
    onUpdated?: () => void;
 | 
					  onUpdated?: () => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = z.object({
 | 
					const schema = z.object({
 | 
				
			||||||
    username: z.string().min(1, { message: 'Username is required' }),
 | 
					  username: z.string().min(1, { message: "Username is required" }),
 | 
				
			||||||
    password: z.string().min(6, { message: 'Password must be at least 6 characters long' }),
 | 
					  password: z
 | 
				
			||||||
 | 
					    .string()
 | 
				
			||||||
 | 
					    .min(6, { message: "Password must be at least 6 characters long" }),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebAccountModal({ data, onUpdated, ...props }: IWebBidModelProps) {
 | 
					export default function WebAccountModal({
 | 
				
			||||||
    const form = useForm({
 | 
					  data,
 | 
				
			||||||
        validate: zodResolver(schema),
 | 
					  onUpdated,
 | 
				
			||||||
    });
 | 
					  ...props
 | 
				
			||||||
 | 
					}: IWebBidModelProps) {
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    validate: zodResolver(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 account will be update`,
 | 
					        message: `This account will be update`,
 | 
				
			||||||
                handleOk: async () => {
 | 
					        handleOk: async () => {
 | 
				
			||||||
                    setLoading(true);
 | 
					          setLoading(true);
 | 
				
			||||||
                    const result = await updateWebBid(values);
 | 
					          const result = await updateWebBid(values);
 | 
				
			||||||
                    setLoading(false);
 | 
					          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 {
 | 
				
			||||||
            setLoading(true);
 | 
					      setLoading(true);
 | 
				
			||||||
            const result = await updateWebBid(values);
 | 
					      const result = await updateWebBid(values);
 | 
				
			||||||
            setLoading(false);
 | 
					      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]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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">Account</span>}
 | 
					      title={<span className="text-xl font-bold">Account</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="Username"
 | 
				
			||||||
 | 
					          {...form.getInputProps("username")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <PasswordInput
 | 
				
			||||||
 | 
					          withAsterisk
 | 
				
			||||||
 | 
					          className="col-span-2"
 | 
				
			||||||
 | 
					          size="sm"
 | 
				
			||||||
 | 
					          label="Password"
 | 
				
			||||||
 | 
					          {...form.getInputProps("password")}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <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 className="col-span-2" size="sm" label="Username" {...form.getInputProps('username')} />
 | 
					        </Button>
 | 
				
			||||||
                <PasswordInput className="col-span-2" size="sm" label="Password" {...form.getInputProps('password')} />
 | 
					      </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>
 | 
					  );
 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,12 +26,14 @@ const schema = {
 | 
				
			||||||
    .number({ message: "Arrival offset seconds is required" })
 | 
					    .number({ message: "Arrival offset seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 60, {
 | 
					    .refine((val) => val >= 60, {
 | 
				
			||||||
      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
					      message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
 | 
				
			||||||
    }).optional(),
 | 
					    })
 | 
				
			||||||
    early_tracking_seconds: z
 | 
					    .optional(),
 | 
				
			||||||
 | 
					  early_tracking_seconds: z
 | 
				
			||||||
    .number({ message: "Early login seconds is required" })
 | 
					    .number({ message: "Early login seconds is required" })
 | 
				
			||||||
    .refine((val) => val >= 600, {
 | 
					    .refine((val) => val >= 600, {
 | 
				
			||||||
      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
					      message: "Early login seconds must be at least 600 seconds (10 minute)",
 | 
				
			||||||
    }).optional(),
 | 
					    })
 | 
				
			||||||
 | 
					    .optional(),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebBidModal({
 | 
					export default function WebBidModal({
 | 
				
			||||||
| 
						 | 
					@ -56,11 +58,7 @@ export default function WebBidModal({
 | 
				
			||||||
        message: `This web will be update`,
 | 
					        message: `This web will be update`,
 | 
				
			||||||
        handleOk: async () => {
 | 
					        handleOk: async () => {
 | 
				
			||||||
          setLoading(true);
 | 
					          setLoading(true);
 | 
				
			||||||
          console.log(
 | 
					
 | 
				
			||||||
            "%csrc/components/web-bid/web-bid-modal.tsx:54 values",
 | 
					 | 
				
			||||||
            "color: #007acc;",
 | 
					 | 
				
			||||||
            values
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          const result = await updateWebBid(values);
 | 
					          const result = await updateWebBid(values);
 | 
				
			||||||
          setLoading(false);
 | 
					          setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,14 +76,19 @@ export default function WebBidModal({
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const { url, origin_url, arrival_offset_seconds, early_tracking_seconds } = values;
 | 
					      const {
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        origin_url,
 | 
				
			||||||
 | 
					        arrival_offset_seconds,
 | 
				
			||||||
 | 
					        early_tracking_seconds,
 | 
				
			||||||
 | 
					      } = values;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setLoading(true);
 | 
					      setLoading(true);
 | 
				
			||||||
      const result = await createWebBid({
 | 
					      const result = await createWebBid({
 | 
				
			||||||
        url,
 | 
					        url,
 | 
				
			||||||
        origin_url,
 | 
					        origin_url,
 | 
				
			||||||
        arrival_offset_seconds,
 | 
					        arrival_offset_seconds,
 | 
				
			||||||
        early_tracking_seconds
 | 
					        early_tracking_seconds,
 | 
				
			||||||
      } as IWebBid);
 | 
					      } as IWebBid);
 | 
				
			||||||
      setLoading(false);
 | 
					      setLoading(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -157,9 +160,9 @@ export default function WebBidModal({
 | 
				
			||||||
          description="Note: that only integer minutes are accepted."
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
          className="col-span-2"
 | 
					          className="col-span-2"
 | 
				
			||||||
          size="sm"
 | 
					          size="sm"
 | 
				
			||||||
          label={`Arrival offset seconds (${
 | 
					          label={`Arrival offset seconds (${formatTimeFromMinutes(
 | 
				
			||||||
             formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60)
 | 
					            form.getValues()["arrival_offset_seconds"] / 60
 | 
				
			||||||
          })`}
 | 
					          )})`}
 | 
				
			||||||
          placeholder="msg: 300"
 | 
					          placeholder="msg: 300"
 | 
				
			||||||
          {...form.getInputProps("arrival_offset_seconds")}
 | 
					          {...form.getInputProps("arrival_offset_seconds")}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					@ -167,9 +170,9 @@ export default function WebBidModal({
 | 
				
			||||||
          description="Note: that only integer minutes are accepted."
 | 
					          description="Note: that only integer minutes are accepted."
 | 
				
			||||||
          className="col-span-2"
 | 
					          className="col-span-2"
 | 
				
			||||||
          size="sm"
 | 
					          size="sm"
 | 
				
			||||||
          label={`Early tracking seconds (${
 | 
					          label={`Early tracking seconds (${formatTimeFromMinutes(
 | 
				
			||||||
             formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60)
 | 
					            form.getValues()["early_tracking_seconds"] / 60
 | 
				
			||||||
          })`}
 | 
					          )})`}
 | 
				
			||||||
          placeholder="msg: 600"
 | 
					          placeholder="msg: 600"
 | 
				
			||||||
          {...form.getInputProps("early_tracking_seconds")}
 | 
					          {...form.getInputProps("early_tracking_seconds")}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,7 +145,7 @@ export default function Bids() {
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      key: "close_time",
 | 
					      key: "close_time_ts",
 | 
				
			||||||
      title: "Close time",
 | 
					      title: "Close time",
 | 
				
			||||||
      typeFilter: "date",
 | 
					      typeFilter: "date",
 | 
				
			||||||
      renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,253 +1,306 @@
 | 
				
			||||||
import { ActionIcon, Badge, Box, Menu, Text } from '@mantine/core';
 | 
					import { ActionIcon, Badge, Box, Menu, Text } from "@mantine/core";
 | 
				
			||||||
import { IconAd, IconAdOff, IconEdit, IconMenu, IconTrash, IconUserEdit } from '@tabler/icons-react';
 | 
					import {
 | 
				
			||||||
import { useMemo, useRef, useState } from 'react';
 | 
					  IconAd,
 | 
				
			||||||
import { deletesWebBid, deleteWebBid, getWebBids, updateWebBid } from '../apis/web-bid';
 | 
					  IconAdOff,
 | 
				
			||||||
import Table from '../lib/table/table';
 | 
					  IconEdit,
 | 
				
			||||||
import { IColumn, TRefTableFn } from '../lib/table/type';
 | 
					  IconMenu,
 | 
				
			||||||
import { useConfirmStore } from '../lib/zustand/use-confirm';
 | 
					  IconSettingsCode,
 | 
				
			||||||
import { IWebBid } from '../system/type';
 | 
					  IconTrash,
 | 
				
			||||||
import { formatTime } from '../utils';
 | 
					  IconUserEdit,
 | 
				
			||||||
import { WebAccountModal, WebBidModal } from '../components/web-bid';
 | 
					} from "@tabler/icons-react";
 | 
				
			||||||
import { useDisclosure } from '@mantine/hooks';
 | 
					import { useMemo, useRef, useState } from "react";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  deletesWebBid,
 | 
				
			||||||
 | 
					  deleteWebBid,
 | 
				
			||||||
 | 
					  getWebBids,
 | 
				
			||||||
 | 
					  updateWebBid,
 | 
				
			||||||
 | 
					} from "../apis/web-bid";
 | 
				
			||||||
 | 
					import Table from "../lib/table/table";
 | 
				
			||||||
 | 
					import { IColumn, TRefTableFn } from "../lib/table/type";
 | 
				
			||||||
 | 
					import { useConfirmStore } from "../lib/zustand/use-confirm";
 | 
				
			||||||
 | 
					import { IWebBid } from "../system/type";
 | 
				
			||||||
 | 
					import { formatTime } from "../utils";
 | 
				
			||||||
 | 
					import { WebAccountModal, WebBidModal } from "../components/web-bid";
 | 
				
			||||||
 | 
					import { useDisclosure } from "@mantine/hooks";
 | 
				
			||||||
 | 
					import ScrapConfigModal from "../components/web-bid/scrap-config.modal";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function WebBids() {
 | 
					export default function WebBids() {
 | 
				
			||||||
    const refTableFn: TRefTableFn<IWebBid> = useRef({});
 | 
					  const refTableFn: TRefTableFn<IWebBid> = useRef({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [clickData, setClickData] = useState<IWebBid | null>(null);
 | 
					  const [clickData, setClickData] = useState<IWebBid | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { setConfirm } = useConfirmStore();
 | 
					  const { setConfirm } = useConfirmStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [webBidOpened, webBidModal] = useDisclosure(false);
 | 
					  const [webBidOpened, webBidModal] = useDisclosure(false);
 | 
				
			||||||
    const [webAccountOpened, webAccountModal] = useDisclosure(false);
 | 
					  const [webAccountOpened, webAccountModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					  const [scrapConfigOpened, scrapConfigModal] = useDisclosure(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const columns: IColumn<IWebBid>[] = [
 | 
					  const columns: IColumn<IWebBid>[] = [
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'id',
 | 
					      key: "id",
 | 
				
			||||||
            title: 'ID',
 | 
					      title: "ID",
 | 
				
			||||||
            typeFilter: 'number',
 | 
					      typeFilter: "number",
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'origin_url',
 | 
					      key: "origin_url",
 | 
				
			||||||
            title: 'Domain',
 | 
					      title: "Domain",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'url',
 | 
					      key: "url",
 | 
				
			||||||
            title: 'Tracking url',
 | 
					      title: "Tracking url",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
            renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
                return <Text>{row.url || 'None'}</Text>;
 | 
					        return <Text>{row.url || "None"}</Text>;
 | 
				
			||||||
            },
 | 
					      },
 | 
				
			||||||
        },
 | 
					    },
 | 
				
			||||||
        {
 | 
					    {
 | 
				
			||||||
            key: 'active',
 | 
					      key: "active",
 | 
				
			||||||
            title: 'Status',
 | 
					      title: "Status",
 | 
				
			||||||
            typeFilter: 'text',
 | 
					      typeFilter: "text",
 | 
				
			||||||
            renderRow(row) {
 | 
					      renderRow(row) {
 | 
				
			||||||
                return (
 | 
					 | 
				
			||||||
                    <Box className="flex items-center justify-center">
 | 
					 | 
				
			||||||
                        <Badge color={row.active ? 'green' : 'red'} size="sm">
 | 
					 | 
				
			||||||
                            {row.active ? 'Enable' : 'Disable'}
 | 
					 | 
				
			||||||
                        </Badge>
 | 
					 | 
				
			||||||
                    </Box>
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            key: 'created_at',
 | 
					 | 
				
			||||||
            title: 'Created at',
 | 
					 | 
				
			||||||
            typeFilter: 'none',
 | 
					 | 
				
			||||||
            renderRow(row) {
 | 
					 | 
				
			||||||
                return <span>{formatTime(row.created_at)}</span>;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            key: 'updated_at',
 | 
					 | 
				
			||||||
            title: 'Update at',
 | 
					 | 
				
			||||||
            typeFilter: 'none',
 | 
					 | 
				
			||||||
            renderRow(row) {
 | 
					 | 
				
			||||||
                return <span>{formatTime(row.updated_at)}</span>;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleDelete = (data: IWebBid) => {
 | 
					 | 
				
			||||||
        setConfirm({
 | 
					 | 
				
			||||||
            title: 'Delete ?',
 | 
					 | 
				
			||||||
            message: 'This web will be delete',
 | 
					 | 
				
			||||||
            handleOk: async () => {
 | 
					 | 
				
			||||||
                await deleteWebBid(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                    refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleToggle = async (data: IWebBid) => {
 | 
					 | 
				
			||||||
        setConfirm({
 | 
					 | 
				
			||||||
            title: (data.active ? 'Disable ' : 'Enable ') + 'ID: ' + data.id,
 | 
					 | 
				
			||||||
            message: 'This web will be ' + (data.active ? 'disable ' : 'enable '),
 | 
					 | 
				
			||||||
            handleOk: async () => {
 | 
					 | 
				
			||||||
                await updateWebBid({ ...data, active: !data.active || false });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                    refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            okButton: {
 | 
					 | 
				
			||||||
                value: data.active ? 'Disable ' : 'Enable ',
 | 
					 | 
				
			||||||
                color: data.active ? 'red' : 'blue',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const table = useMemo(() => {
 | 
					 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <Table
 | 
					          <Box className="flex items-center justify-center">
 | 
				
			||||||
                actionsOptions={{
 | 
					            <Badge color={row.active ? "green" : "red"} size="sm">
 | 
				
			||||||
                    actions: [
 | 
					              {row.active ? "Enable" : "Disable"}
 | 
				
			||||||
                        {
 | 
					            </Badge>
 | 
				
			||||||
                            key: 'add',
 | 
					          </Box>
 | 
				
			||||||
                            title: 'Add',
 | 
					 | 
				
			||||||
                            callback: () => {
 | 
					 | 
				
			||||||
                                webBidModal.open();
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            key: 'delete',
 | 
					 | 
				
			||||||
                            title: 'Delete',
 | 
					 | 
				
			||||||
                            callback: (data) => {
 | 
					 | 
				
			||||||
                                if (!data.length) return;
 | 
					 | 
				
			||||||
                                setConfirm({
 | 
					 | 
				
			||||||
                                    title: 'Delete',
 | 
					 | 
				
			||||||
                                    message: `${data.length} will be delete`,
 | 
					 | 
				
			||||||
                                    handleOk: async () => {
 | 
					 | 
				
			||||||
                                        const result = await deletesWebBid(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                        if (!result) return;
 | 
					 | 
				
			||||||
                                        if (refTableFn.current.fetchData) {
 | 
					 | 
				
			||||||
                                            refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
                                    },
 | 
					 | 
				
			||||||
                                });
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            disabled: (data) => data.length <= 0,
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                refTableFn={refTableFn}
 | 
					 | 
				
			||||||
                striped
 | 
					 | 
				
			||||||
                showLoading={true}
 | 
					 | 
				
			||||||
                highlightOnHover
 | 
					 | 
				
			||||||
                styleDefaultHead={{
 | 
					 | 
				
			||||||
                    justifyContent: 'flex-start',
 | 
					 | 
				
			||||||
                    width: 'fit-content',
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                options={{
 | 
					 | 
				
			||||||
                    query: getWebBids,
 | 
					 | 
				
			||||||
                    pathToData: 'data.data',
 | 
					 | 
				
			||||||
                    keyOptions: {
 | 
					 | 
				
			||||||
                        last_page: 'lastPage',
 | 
					 | 
				
			||||||
                        per_page: 'perPage',
 | 
					 | 
				
			||||||
                        from: 'from',
 | 
					 | 
				
			||||||
                        to: 'to',
 | 
					 | 
				
			||||||
                        total: 'total',
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                rows={[]}
 | 
					 | 
				
			||||||
                withColumnBorders
 | 
					 | 
				
			||||||
                showChooses={true}
 | 
					 | 
				
			||||||
                withTableBorder
 | 
					 | 
				
			||||||
                columns={columns}
 | 
					 | 
				
			||||||
                actions={{
 | 
					 | 
				
			||||||
                    title: <Box className="w-full text-center">Action</Box>,
 | 
					 | 
				
			||||||
                    body: (row) => {
 | 
					 | 
				
			||||||
                        return (
 | 
					 | 
				
			||||||
                            <Menu shadow="md" width={200}>
 | 
					 | 
				
			||||||
                                <Menu.Target>
 | 
					 | 
				
			||||||
                                    <Box className="flex w-full items-center justify-center">
 | 
					 | 
				
			||||||
                                        <ActionIcon size="sm" variant="light">
 | 
					 | 
				
			||||||
                                            <IconMenu size={14} />
 | 
					 | 
				
			||||||
                                        </ActionIcon>
 | 
					 | 
				
			||||||
                                    </Box>
 | 
					 | 
				
			||||||
                                </Menu.Target>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                <Menu.Dropdown>
 | 
					 | 
				
			||||||
                                    <Menu.Item
 | 
					 | 
				
			||||||
                                        onClick={() => {
 | 
					 | 
				
			||||||
                                            setClickData(row);
 | 
					 | 
				
			||||||
                                            webBidModal.open();
 | 
					 | 
				
			||||||
                                        }}
 | 
					 | 
				
			||||||
                                        leftSection={<IconEdit size={14} />}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        Edit
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
                                    <Menu.Item
 | 
					 | 
				
			||||||
                                        onClick={() => {
 | 
					 | 
				
			||||||
                                            setClickData(row);
 | 
					 | 
				
			||||||
                                            webAccountModal.open();
 | 
					 | 
				
			||||||
                                        }}
 | 
					 | 
				
			||||||
                                        leftSection={<IconUserEdit size={14} />}
 | 
					 | 
				
			||||||
                                    >
 | 
					 | 
				
			||||||
                                        Account
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    <Menu.Item onClick={() => handleToggle(row)} leftSection={row.active ? <IconAdOff size={14} /> : <IconAd size={14} />}>
 | 
					 | 
				
			||||||
                                        {row.active ? 'Disable' : 'Enable'}
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    <Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
 | 
					 | 
				
			||||||
                                        Delete
 | 
					 | 
				
			||||||
                                    </Menu.Item>
 | 
					 | 
				
			||||||
                                </Menu.Dropdown>
 | 
					 | 
				
			||||||
                            </Menu>
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                rowKey="id"
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					      },
 | 
				
			||||||
    }, []);
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "created_at",
 | 
				
			||||||
 | 
					      title: "Created at",
 | 
				
			||||||
 | 
					      typeFilter: "none",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return <span>{formatTime(row.created_at)}</span>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: "updated_at",
 | 
				
			||||||
 | 
					      title: "Update at",
 | 
				
			||||||
 | 
					      typeFilter: "none",
 | 
				
			||||||
 | 
					      renderRow(row) {
 | 
				
			||||||
 | 
					        return <span>{formatTime(row.updated_at)}</span>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDelete = (data: IWebBid) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: "Delete ?",
 | 
				
			||||||
 | 
					      message: "This web will be delete",
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        await deleteWebBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					          refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggle = async (data: IWebBid) => {
 | 
				
			||||||
 | 
					    setConfirm({
 | 
				
			||||||
 | 
					      title: (data.active ? "Disable " : "Enable ") + "ID: " + data.id,
 | 
				
			||||||
 | 
					      message: "This web will be " + (data.active ? "disable " : "enable "),
 | 
				
			||||||
 | 
					      handleOk: async () => {
 | 
				
			||||||
 | 
					        await updateWebBid({ ...data, active: !data.active || false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					          refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      okButton: {
 | 
				
			||||||
 | 
					        value: data.active ? "Disable " : "Enable ",
 | 
				
			||||||
 | 
					        color: data.active ? "red" : "blue",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const table = useMemo(() => {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <Box>
 | 
					      <Table
 | 
				
			||||||
            {table}
 | 
					        actionsOptions={{
 | 
				
			||||||
 | 
					          actions: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              key: "add",
 | 
				
			||||||
 | 
					              title: "Add",
 | 
				
			||||||
 | 
					              callback: () => {
 | 
				
			||||||
 | 
					                webBidModal.open();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              key: "delete",
 | 
				
			||||||
 | 
					              title: "Delete",
 | 
				
			||||||
 | 
					              callback: (data) => {
 | 
				
			||||||
 | 
					                if (!data.length) return;
 | 
				
			||||||
 | 
					                setConfirm({
 | 
				
			||||||
 | 
					                  title: "Delete",
 | 
				
			||||||
 | 
					                  message: `${data.length} will be delete`,
 | 
				
			||||||
 | 
					                  handleOk: async () => {
 | 
				
			||||||
 | 
					                    const result = await deletesWebBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <WebBidModal
 | 
					                    if (!result) return;
 | 
				
			||||||
                data={clickData}
 | 
					                    if (refTableFn.current.fetchData) {
 | 
				
			||||||
                opened={webBidOpened}
 | 
					                      refTableFn.current.fetchData();
 | 
				
			||||||
                onClose={() => {
 | 
					 | 
				
			||||||
                    webBidModal.close();
 | 
					 | 
				
			||||||
                    setClickData(null);
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onUpdated={() => {
 | 
					 | 
				
			||||||
                    setClickData(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (refTableFn.current?.fetchData) {
 | 
					 | 
				
			||||||
                        refTableFn.current.fetchData();
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }}
 | 
					                  },
 | 
				
			||||||
            />
 | 
					                });
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              disabled: (data) => data.length <= 0,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        refTableFn={refTableFn}
 | 
				
			||||||
 | 
					        striped
 | 
				
			||||||
 | 
					        showLoading={true}
 | 
				
			||||||
 | 
					        highlightOnHover
 | 
				
			||||||
 | 
					        styleDefaultHead={{
 | 
				
			||||||
 | 
					          justifyContent: "flex-start",
 | 
				
			||||||
 | 
					          width: "fit-content",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        options={{
 | 
				
			||||||
 | 
					          query: getWebBids,
 | 
				
			||||||
 | 
					          pathToData: "data.data",
 | 
				
			||||||
 | 
					          keyOptions: {
 | 
				
			||||||
 | 
					            last_page: "lastPage",
 | 
				
			||||||
 | 
					            per_page: "perPage",
 | 
				
			||||||
 | 
					            from: "from",
 | 
				
			||||||
 | 
					            to: "to",
 | 
				
			||||||
 | 
					            total: "total",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        rows={[]}
 | 
				
			||||||
 | 
					        withColumnBorders
 | 
				
			||||||
 | 
					        showChooses={true}
 | 
				
			||||||
 | 
					        withTableBorder
 | 
				
			||||||
 | 
					        columns={columns}
 | 
				
			||||||
 | 
					        actions={{
 | 
				
			||||||
 | 
					          title: <Box className="w-full text-center">Action</Box>,
 | 
				
			||||||
 | 
					          body: (row) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <Menu shadow="md" width={200}>
 | 
				
			||||||
 | 
					                <Menu.Target>
 | 
				
			||||||
 | 
					                  <Box className="flex w-full items-center justify-center">
 | 
				
			||||||
 | 
					                    <ActionIcon size="sm" variant="light">
 | 
				
			||||||
 | 
					                      <IconMenu size={14} />
 | 
				
			||||||
 | 
					                    </ActionIcon>
 | 
				
			||||||
 | 
					                  </Box>
 | 
				
			||||||
 | 
					                </Menu.Target>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <WebAccountModal
 | 
					                <Menu.Dropdown>
 | 
				
			||||||
                data={clickData}
 | 
					                  <Menu.Item
 | 
				
			||||||
                opened={webAccountOpened}
 | 
					                    onClick={() => {
 | 
				
			||||||
                onClose={() => {
 | 
					                      setClickData(row);
 | 
				
			||||||
                    webAccountModal.close();
 | 
					                      webBidModal.open();
 | 
				
			||||||
                    setClickData(null);
 | 
					                    }}
 | 
				
			||||||
                }}
 | 
					                    leftSection={<IconEdit size={14} />}
 | 
				
			||||||
                onUpdated={() => {
 | 
					                  >
 | 
				
			||||||
                    setClickData(null);
 | 
					                    Edit
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      setClickData(row);
 | 
				
			||||||
 | 
					                      webAccountModal.open();
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    leftSection={<IconUserEdit size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Account
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (refTableFn.current?.fetchData) {
 | 
					                  <Menu.Item
 | 
				
			||||||
                        refTableFn.current.fetchData();
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      setClickData(row);
 | 
				
			||||||
 | 
					                      scrapConfigModal.open();
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                    leftSection={<IconSettingsCode size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Scrap config
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => handleToggle(row)}
 | 
				
			||||||
 | 
					                    leftSection={
 | 
				
			||||||
 | 
					                      row.active ? (
 | 
				
			||||||
 | 
					                        <IconAdOff size={14} />
 | 
				
			||||||
 | 
					                      ) : (
 | 
				
			||||||
 | 
					                        <IconAd size={14} />
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }}
 | 
					                  >
 | 
				
			||||||
            />
 | 
					                    {row.active ? "Disable" : "Enable"}
 | 
				
			||||||
        </Box>
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <Menu.Item
 | 
				
			||||||
 | 
					                    onClick={() => handleDelete(row)}
 | 
				
			||||||
 | 
					                    leftSection={<IconTrash color="red" size={14} />}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    Delete
 | 
				
			||||||
 | 
					                  </Menu.Item>
 | 
				
			||||||
 | 
					                </Menu.Dropdown>
 | 
				
			||||||
 | 
					              </Menu>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        rowKey="id"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box>
 | 
				
			||||||
 | 
					      {table}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <WebBidModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={webBidOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          webBidModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <WebAccountModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={webAccountOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          webAccountModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <ScrapConfigModal
 | 
				
			||||||
 | 
					        data={clickData}
 | 
				
			||||||
 | 
					        opened={scrapConfigOpened}
 | 
				
			||||||
 | 
					        onClose={() => {
 | 
				
			||||||
 | 
					          scrapConfigModal.close();
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					        onUpdated={() => {
 | 
				
			||||||
 | 
					          setClickData(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (refTableFn.current?.fetchData) {
 | 
				
			||||||
 | 
					            refTableFn.current.fetchData();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,79 +1,94 @@
 | 
				
			||||||
export interface IAdmin extends ITimestamp {
 | 
					export interface IAdmin extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    email: string;
 | 
					  email: string;
 | 
				
			||||||
    username: string;
 | 
					  username: string;
 | 
				
			||||||
    fullname: string;
 | 
					  fullname: string;
 | 
				
			||||||
    password?: string;
 | 
					  password?: string;
 | 
				
			||||||
    is_system_account: boolean;
 | 
					  is_system_account: boolean;
 | 
				
			||||||
    permissions: [];
 | 
					  permissions: [];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IKey extends ITimestamp {
 | 
					export interface IKey extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    client_key: string;
 | 
					  client_key: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ITimestamp {
 | 
					export interface ITimestamp {
 | 
				
			||||||
    created_at: string;
 | 
					  created_at: string;
 | 
				
			||||||
    updated_at: string;
 | 
					  updated_at: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IHistory extends ITimestamp {
 | 
					export interface IHistory extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    price: number;
 | 
					  price: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IOutBidLog extends ITimestamp {
 | 
					export interface IOutBidLog extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    model: string;
 | 
					  model: string;
 | 
				
			||||||
    lot_id: string;
 | 
					  lot_id: string;
 | 
				
			||||||
    out_price: number;
 | 
					  out_price: number;
 | 
				
			||||||
    raw_data: string;
 | 
					  raw_data: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IScrapConfig extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					  scrap_items: IScrapItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IScrapItem extends ITimestamp {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					  image_url: string | null;
 | 
				
			||||||
 | 
					  keyword: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IWebBid extends ITimestamp {
 | 
					export interface IWebBid extends ITimestamp {
 | 
				
			||||||
    created_at: string;
 | 
					  created_at: string;
 | 
				
			||||||
    updated_at: string;
 | 
					  updated_at: string;
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    origin_url: string;
 | 
					  origin_url: string;
 | 
				
			||||||
    url: string | null;
 | 
					  url: string | null;
 | 
				
			||||||
    username: string | null;
 | 
					  username: string | null;
 | 
				
			||||||
    password: string | null;
 | 
					  password: string | null;
 | 
				
			||||||
    active: boolean;
 | 
					  active: boolean;
 | 
				
			||||||
    arrival_offset_seconds: number;
 | 
					  arrival_offset_seconds: number;
 | 
				
			||||||
    early_tracking_seconds: number;
 | 
					  early_tracking_seconds: number;
 | 
				
			||||||
    snapshot_at: string | null
 | 
					  snapshot_at: string | null;
 | 
				
			||||||
    children: IBid[];
 | 
					  children: IBid[];
 | 
				
			||||||
 | 
					  scrap_config: IScrapConfig;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IBid extends ITimestamp {
 | 
					export interface IBid extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    max_price: number;
 | 
					  max_price: number;
 | 
				
			||||||
    reserve_price: number;
 | 
					  reserve_price: number;
 | 
				
			||||||
    current_price: number;
 | 
					  current_price: number;
 | 
				
			||||||
    name: string | null;
 | 
					  name: string | null;
 | 
				
			||||||
    quantity: number;
 | 
					  quantity: number;
 | 
				
			||||||
    url: string;
 | 
					  url: string;
 | 
				
			||||||
    model: string;
 | 
					  model: string;
 | 
				
			||||||
    lot_id: string;
 | 
					  lot_id: string;
 | 
				
			||||||
    plus_price: number;
 | 
					  plus_price: number;
 | 
				
			||||||
    close_time: string | null;
 | 
					  close_time: string | null;
 | 
				
			||||||
    start_bid_time: string | null;
 | 
					  close_time_ts: string | null;
 | 
				
			||||||
    first_bid: boolean;
 | 
					  start_bid_time: string | null;
 | 
				
			||||||
    status: 'biding' | 'out-bid' | 'win-bid';
 | 
					  first_bid: boolean;
 | 
				
			||||||
    histories: IHistory[];
 | 
					  status: "biding" | "out-bid" | "win-bid";
 | 
				
			||||||
    web_bid: IWebBid;
 | 
					  histories: IHistory[];
 | 
				
			||||||
 | 
					  web_bid: IWebBid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPermission extends ITimestamp {
 | 
					export interface IPermission extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    name: string;
 | 
					  name: string;
 | 
				
			||||||
    description: string;
 | 
					  description: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export interface ISendMessageHistory extends ITimestamp {
 | 
					export interface ISendMessageHistory extends ITimestamp {
 | 
				
			||||||
    id: number;
 | 
					  id: number;
 | 
				
			||||||
    message: string;
 | 
					  message: string;
 | 
				
			||||||
    bid: IBid;
 | 
					  bid: IBid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
{"createdAt":1747292824357}
 | 
					{"createdAt":1747701959077}
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@
 | 
				
			||||||
        "@nestjs/websockets": "^11.0.11",
 | 
					        "@nestjs/websockets": "^11.0.11",
 | 
				
			||||||
        "axios": "^1.8.3",
 | 
					        "axios": "^1.8.3",
 | 
				
			||||||
        "bcrypt": "^5.1.1",
 | 
					        "bcrypt": "^5.1.1",
 | 
				
			||||||
 | 
					        "cheerio": "^1.0.0",
 | 
				
			||||||
        "class-transformer": "^0.5.1",
 | 
					        "class-transformer": "^0.5.1",
 | 
				
			||||||
        "class-validator": "^0.14.1",
 | 
					        "class-validator": "^0.14.1",
 | 
				
			||||||
        "cookie": "^1.0.2",
 | 
					        "cookie": "^1.0.2",
 | 
				
			||||||
| 
						 | 
					@ -4165,6 +4166,12 @@
 | 
				
			||||||
      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
 | 
					      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/boolbase": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
 | 
				
			||||||
 | 
					      "license": "ISC"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/brace-expansion": {
 | 
					    "node_modules/brace-expansion": {
 | 
				
			||||||
      "version": "2.0.1",
 | 
					      "version": "2.0.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -4422,6 +4429,48 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cheerio": {
 | 
				
			||||||
 | 
					      "version": "1.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "cheerio-select": "^2.1.0",
 | 
				
			||||||
 | 
					        "dom-serializer": "^2.0.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.3",
 | 
				
			||||||
 | 
					        "domutils": "^3.1.0",
 | 
				
			||||||
 | 
					        "encoding-sniffer": "^0.2.0",
 | 
				
			||||||
 | 
					        "htmlparser2": "^9.1.0",
 | 
				
			||||||
 | 
					        "parse5": "^7.1.2",
 | 
				
			||||||
 | 
					        "parse5-htmlparser2-tree-adapter": "^7.0.0",
 | 
				
			||||||
 | 
					        "parse5-parser-stream": "^7.1.2",
 | 
				
			||||||
 | 
					        "undici": "^6.19.5",
 | 
				
			||||||
 | 
					        "whatwg-mimetype": "^4.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18.17"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cheerio-select": {
 | 
				
			||||||
 | 
					      "version": "2.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "boolbase": "^1.0.0",
 | 
				
			||||||
 | 
					        "css-select": "^5.1.0",
 | 
				
			||||||
 | 
					        "css-what": "^6.1.0",
 | 
				
			||||||
 | 
					        "domelementtype": "^2.3.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.3",
 | 
				
			||||||
 | 
					        "domutils": "^3.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/fb55"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/chokidar": {
 | 
					    "node_modules/chokidar": {
 | 
				
			||||||
      "version": "3.6.0",
 | 
					      "version": "3.6.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -4923,6 +4972,34 @@
 | 
				
			||||||
        "node": ">= 8"
 | 
					        "node": ">= 8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/css-select": {
 | 
				
			||||||
 | 
					      "version": "5.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "boolbase": "^1.0.0",
 | 
				
			||||||
 | 
					        "css-what": "^6.1.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.2",
 | 
				
			||||||
 | 
					        "domutils": "^3.0.1",
 | 
				
			||||||
 | 
					        "nth-check": "^2.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/fb55"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/css-what": {
 | 
				
			||||||
 | 
					      "version": "6.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 6"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/fb55"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/dayjs": {
 | 
					    "node_modules/dayjs": {
 | 
				
			||||||
      "version": "1.11.13",
 | 
					      "version": "1.11.13",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5138,6 +5215,61 @@
 | 
				
			||||||
        "node": ">=6.0.0"
 | 
					        "node": ">=6.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/dom-serializer": {
 | 
				
			||||||
 | 
					      "version": "2.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "domelementtype": "^2.3.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.2",
 | 
				
			||||||
 | 
					        "entities": "^4.2.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/domelementtype": {
 | 
				
			||||||
 | 
					      "version": "2.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
 | 
				
			||||||
 | 
					      "funding": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "github",
 | 
				
			||||||
 | 
					          "url": "https://github.com/sponsors/fb55"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/domhandler": {
 | 
				
			||||||
 | 
					      "version": "5.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "domelementtype": "^2.3.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 4"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/domhandler?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/domutils": {
 | 
				
			||||||
 | 
					      "version": "3.2.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "dom-serializer": "^2.0.0",
 | 
				
			||||||
 | 
					        "domelementtype": "^2.3.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.3"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/domutils?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/dotenv": {
 | 
					    "node_modules/dotenv": {
 | 
				
			||||||
      "version": "16.4.7",
 | 
					      "version": "16.4.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5260,6 +5392,31 @@
 | 
				
			||||||
        "iconv-lite": "^0.6.2"
 | 
					        "iconv-lite": "^0.6.2"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/encoding-sniffer": {
 | 
				
			||||||
 | 
					      "version": "0.2.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "iconv-lite": "^0.6.3",
 | 
				
			||||||
 | 
					        "whatwg-encoding": "^3.1.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/encoding-sniffer/node_modules/iconv-lite": {
 | 
				
			||||||
 | 
					      "version": "0.6.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "safer-buffer": ">= 2.1.2 < 3.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/encoding/node_modules/iconv-lite": {
 | 
					    "node_modules/encoding/node_modules/iconv-lite": {
 | 
				
			||||||
      "version": "0.6.3",
 | 
					      "version": "0.6.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -5341,6 +5498,18 @@
 | 
				
			||||||
        "node": ">=10.13.0"
 | 
					        "node": ">=10.13.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/entities": {
 | 
				
			||||||
 | 
					      "version": "4.5.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.12"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/entities?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/error-ex": {
 | 
					    "node_modules/error-ex": {
 | 
				
			||||||
      "version": "1.3.2",
 | 
					      "version": "1.3.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
 | 
				
			||||||
| 
						 | 
					@ -6773,6 +6942,25 @@
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/htmlparser2": {
 | 
				
			||||||
 | 
					      "version": "9.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
 | 
				
			||||||
 | 
					      "funding": [
 | 
				
			||||||
 | 
					        "https://github.com/fb55/htmlparser2?sponsor=1",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "github",
 | 
				
			||||||
 | 
					          "url": "https://github.com/sponsors/fb55"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "domelementtype": "^2.3.0",
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.3",
 | 
				
			||||||
 | 
					        "domutils": "^3.1.0",
 | 
				
			||||||
 | 
					        "entities": "^4.5.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/http-errors": {
 | 
					    "node_modules/http-errors": {
 | 
				
			||||||
      "version": "2.0.0",
 | 
					      "version": "2.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					@ -8900,6 +9088,18 @@
 | 
				
			||||||
        "set-blocking": "^2.0.0"
 | 
					        "set-blocking": "^2.0.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/nth-check": {
 | 
				
			||||||
 | 
					      "version": "2.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "boolbase": "^1.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/nth-check?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/object-assign": {
 | 
					    "node_modules/object-assign": {
 | 
				
			||||||
      "version": "4.1.1",
 | 
					      "version": "4.1.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
 | 
				
			||||||
| 
						 | 
					@ -9109,6 +9309,55 @@
 | 
				
			||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/parse5": {
 | 
				
			||||||
 | 
					      "version": "7.3.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "entities": "^6.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/inikulin/parse5?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/parse5-htmlparser2-tree-adapter": {
 | 
				
			||||||
 | 
					      "version": "7.1.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "domhandler": "^5.0.3",
 | 
				
			||||||
 | 
					        "parse5": "^7.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/inikulin/parse5?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/parse5-parser-stream": {
 | 
				
			||||||
 | 
					      "version": "7.1.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "parse5": "^7.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/inikulin/parse5?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/parse5/node_modules/entities": {
 | 
				
			||||||
 | 
					      "version": "6.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
 | 
				
			||||||
 | 
					      "license": "BSD-2-Clause",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.12"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/fb55/entities?sponsor=1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/parseurl": {
 | 
					    "node_modules/parseurl": {
 | 
				
			||||||
      "version": "1.3.3",
 | 
					      "version": "1.3.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 | 
				
			||||||
| 
						 | 
					@ -11532,6 +11781,15 @@
 | 
				
			||||||
        "node": ">=8"
 | 
					        "node": ">=8"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/undici": {
 | 
				
			||||||
 | 
					      "version": "6.21.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18.17"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/undici-types": {
 | 
					    "node_modules/undici-types": {
 | 
				
			||||||
      "version": "6.19.8",
 | 
					      "version": "6.19.8",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
 | 
				
			||||||
| 
						 | 
					@ -11842,6 +12100,39 @@
 | 
				
			||||||
        "url": "https://opencollective.com/webpack"
 | 
					        "url": "https://opencollective.com/webpack"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/whatwg-encoding": {
 | 
				
			||||||
 | 
					      "version": "3.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "iconv-lite": "0.6.3"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/whatwg-encoding/node_modules/iconv-lite": {
 | 
				
			||||||
 | 
					      "version": "0.6.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "safer-buffer": ">= 2.1.2 < 3.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/whatwg-mimetype": {
 | 
				
			||||||
 | 
					      "version": "4.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=18"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/whatwg-url": {
 | 
					    "node_modules/whatwg-url": {
 | 
				
			||||||
      "version": "5.0.0",
 | 
					      "version": "5.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,6 +39,7 @@
 | 
				
			||||||
    "@nestjs/websockets": "^11.0.11",
 | 
					    "@nestjs/websockets": "^11.0.11",
 | 
				
			||||||
    "axios": "^1.8.3",
 | 
					    "axios": "^1.8.3",
 | 
				
			||||||
    "bcrypt": "^5.1.1",
 | 
					    "bcrypt": "^5.1.1",
 | 
				
			||||||
 | 
					    "cheerio": "^1.0.0",
 | 
				
			||||||
    "class-transformer": "^0.5.1",
 | 
					    "class-transformer": "^0.5.1",
 | 
				
			||||||
    "class-validator": "^0.14.1",
 | 
					    "class-validator": "^0.14.1",
 | 
				
			||||||
    "cookie": "^1.0.2",
 | 
					    "cookie": "^1.0.2",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import {
 | 
				
			||||||
import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
					import { AuthorizationMiddleware } from './modules/admins/middlewares/authorization.middleware';
 | 
				
			||||||
import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
					import { ClientAuthenticationMiddleware } from './modules/auth/middlewares/client-authentication.middleware';
 | 
				
			||||||
import { NotificationModule } from './modules/notification/notification.module';
 | 
					import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
 | 
					import { ScrapsModule } from './modules/scraps/scraps.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
| 
						 | 
					@ -24,6 +25,7 @@ import { NotificationModule } from './modules/notification/notification.module';
 | 
				
			||||||
    AuthModule,
 | 
					    AuthModule,
 | 
				
			||||||
    AdminsModule,
 | 
					    AdminsModule,
 | 
				
			||||||
    NotificationModule,
 | 
					    NotificationModule,
 | 
				
			||||||
 | 
					    ScrapsModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [],
 | 
					  controllers: [],
 | 
				
			||||||
  providers: [],
 | 
					  providers: [],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,9 @@ export class Bid extends Timestamp {
 | 
				
			||||||
  @Column({ default: null, nullable: true })
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
  close_time: string;
 | 
					  close_time: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
 | 
					  close_time_ts: Date | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ default: null, nullable: true })
 | 
					  @Column({ default: null, nullable: true })
 | 
				
			||||||
  start_bid_time: string;
 | 
					  start_bid_time: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,15 @@
 | 
				
			||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  OneToMany,
 | 
				
			||||||
 | 
					  OneToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
import { Timestamp } from './timestamp';
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
import { Bid } from './bid.entity';
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
import { Exclude } from 'class-transformer';
 | 
					import { Exclude } from 'class-transformer';
 | 
				
			||||||
 | 
					import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '@/modules/scraps/entities/scrap-config.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('web_bids')
 | 
					@Entity('web_bids')
 | 
				
			||||||
export class WebBid extends Timestamp {
 | 
					export class WebBid extends Timestamp {
 | 
				
			||||||
| 
						 | 
					@ -37,4 +45,7 @@ export class WebBid extends Timestamp {
 | 
				
			||||||
    cascade: true,
 | 
					    cascade: true,
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  children: Bid[];
 | 
					  children: Bid[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => ScrapConfig, (scrap) => scrap.web_bid)
 | 
				
			||||||
 | 
					  scrap_config: ScrapConfig;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +65,7 @@ export class BidsService {
 | 
				
			||||||
      sortableColumns: [
 | 
					      sortableColumns: [
 | 
				
			||||||
        'id',
 | 
					        'id',
 | 
				
			||||||
        'close_time',
 | 
					        'close_time',
 | 
				
			||||||
 | 
					        'close_time_ts',
 | 
				
			||||||
        'first_bid',
 | 
					        'first_bid',
 | 
				
			||||||
        'model',
 | 
					        'model',
 | 
				
			||||||
        'lot_id',
 | 
					        'lot_id',
 | 
				
			||||||
| 
						 | 
					@ -120,7 +121,9 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.emitAllBidEvent();
 | 
					    await this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(response ? response(result) : plainToClass(Bid, result));
 | 
					    return AppResponse.toResponse(
 | 
				
			||||||
 | 
					      response ? response(result) : plainToClass(Bid, result),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async update(id: Bid['id'], data: UpdateBidDto) {
 | 
					  async update(id: Bid['id'], data: UpdateBidDto) {
 | 
				
			||||||
| 
						 | 
					@ -266,6 +269,7 @@ export class BidsService {
 | 
				
			||||||
      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
					      new Date(close_time).getTime() > new Date(bid.close_time).getTime()
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      bid.close_time = close_time;
 | 
					      bid.close_time = close_time;
 | 
				
			||||||
 | 
					      bid.close_time_ts = new Date(close_time);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
 | 
					    // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model
 | 
				
			||||||
| 
						 | 
					@ -550,11 +554,17 @@ export class BidsService {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getBidByModel(model: string) {
 | 
					  async getBidByModel(model: string) {
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
    console.log('%csrc/modules/bids/services/bids.service.ts:554 model', 'color: #007acc;', model);
 | 
					      '%csrc/modules/bids/services/bids.service.ts:554 model',
 | 
				
			||||||
 | 
					      'color: #007acc;',
 | 
				
			||||||
 | 
					      model,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    const bid = await this.bidsRepo.findOne({ where: { model } });
 | 
					    const bid = await this.bidsRepo.findOne({ where: { model } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!bid) return AppResponse.toResponse(null, {status_code: HttpStatus.NOT_FOUND});
 | 
					    if (!bid)
 | 
				
			||||||
 | 
					      return AppResponse.toResponse(null, {
 | 
				
			||||||
 | 
					        status_code: HttpStatus.NOT_FOUND,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(plainToClass(Bid, bid));
 | 
					    return AppResponse.toResponse(plainToClass(Bid, bid));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ export class TasksService {
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Cron(CronExpression.EVERY_MINUTE)
 | 
					  @Cron(CronExpression.EVERY_MINUTE)
 | 
				
			||||||
  async handleCron() {
 | 
					  async handleResetTool() {
 | 
				
			||||||
    const bids = await this.bidsService.bidsRepo.find({
 | 
					    const bids = await this.bidsService.bidsRepo.find({
 | 
				
			||||||
      where: { status: 'biding' },
 | 
					      where: { status: 'biding' },
 | 
				
			||||||
      select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
 | 
					      select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,6 +53,9 @@ export class WebBidsService {
 | 
				
			||||||
      filterableColumns,
 | 
					      filterableColumns,
 | 
				
			||||||
      defaultSortBy: [['id', 'DESC']],
 | 
					      defaultSortBy: [['id', 'DESC']],
 | 
				
			||||||
      maxLimit: 100,
 | 
					      maxLimit: 100,
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        scrap_config: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toPagination<WebBid>(data, true, WebBid);
 | 
					    return AppResponse.toPagination<WebBid>(data, true, WebBid);
 | 
				
			||||||
| 
						 | 
					@ -66,6 +69,11 @@ export class WebBidsService {
 | 
				
			||||||
        children: { status: 'biding' },
 | 
					        children: { status: 'biding' },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      relations: { children: { histories: true, web_bid: true } },
 | 
					      relations: { children: { histories: true, web_bid: true } },
 | 
				
			||||||
 | 
					      order: {
 | 
				
			||||||
 | 
					        children: {
 | 
				
			||||||
 | 
					          close_time_ts: 'ASC',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from '../services/scrap-config.service';
 | 
				
			||||||
 | 
					import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '../entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import { IsNull, Not } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItemsService } from '../services/scrap-item-config.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('admin/scrap-configs')
 | 
				
			||||||
 | 
					export class ScrapConfigsController {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private readonly scrapConfigsService: ScrapConfigsService,
 | 
				
			||||||
 | 
					    private readonly scrapItemsService: ScrapItemsService,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post()
 | 
				
			||||||
 | 
					  async create(@Body() data: CreateScrapConfigDto) {
 | 
				
			||||||
 | 
					    return await this.scrapConfigsService.create(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put(':id')
 | 
				
			||||||
 | 
					  async update(
 | 
				
			||||||
 | 
					    @Param('id') id: ScrapConfig['id'],
 | 
				
			||||||
 | 
					    @Body() data: UpdateScrapConfigDto,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    return await this.scrapConfigsService.update(id, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  async test() {
 | 
				
			||||||
 | 
					    const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        search_url: Not(IsNull()),
 | 
				
			||||||
 | 
					        keywords: Not(IsNull()),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        web_bid: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const models = this.scrapConfigsService.scrapModels(scrapConfigs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.allSettled(
 | 
				
			||||||
 | 
					      models.map(async (item) => {
 | 
				
			||||||
 | 
					        await item.action();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.keys(item.results).forEach(async (key) => {
 | 
				
			||||||
 | 
					          const data = item.results[key];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          await this.scrapItemsService.scrapItemRepo.upsert(data, [
 | 
				
			||||||
 | 
					            'model',
 | 
				
			||||||
 | 
					            'scrap_config',
 | 
				
			||||||
 | 
					          ]);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { a: 'abc' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import { IsNumber, IsOptional, IsString, IsUrl } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class CreateScrapConfigDto {
 | 
				
			||||||
 | 
					  @IsUrl()
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  web_id: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					import { PartialType } from '@nestjs/mapped-types';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from './create-scrap-config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UpdateScrapConfigDto extends PartialType(CreateScrapConfigDto) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  JoinColumn,
 | 
				
			||||||
 | 
					  OneToMany,
 | 
				
			||||||
 | 
					  OneToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItem } from './scrap-item.entity';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('scrap-configs')
 | 
				
			||||||
 | 
					export class ScrapConfig extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ unique: true })
 | 
				
			||||||
 | 
					  search_url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ default: 'cisco' })
 | 
				
			||||||
 | 
					  keywords: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToOne(() => WebBid, (web) => web.scrap_config, { onDelete: 'CASCADE' })
 | 
				
			||||||
 | 
					  @JoinColumn()
 | 
				
			||||||
 | 
					  web_bid: WebBid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @OneToMany(() => ScrapItem, (web) => web.scrap_config, {
 | 
				
			||||||
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  scrap_items: ScrapItem[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  ManyToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfig } from './scrap-config.entity';
 | 
				
			||||||
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity('scrap-items')
 | 
				
			||||||
 | 
					@Unique(['model', 'scrap_config'])
 | 
				
			||||||
 | 
					export class ScrapItem extends Timestamp {
 | 
				
			||||||
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column()
 | 
				
			||||||
 | 
					  model: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null })
 | 
				
			||||||
 | 
					  image_url: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null })
 | 
				
			||||||
 | 
					  keyword: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Column({ nullable: true, default: null })
 | 
				
			||||||
 | 
					  current_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ManyToOne(() => ScrapConfig, (web) => web.scrap_items, {
 | 
				
			||||||
 | 
					    onDelete: 'CASCADE',
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  scrap_config: ScrapConfig;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
 | 
				
			||||||
 | 
					export abstract class Timestamp {
 | 
				
			||||||
 | 
					  @CreateDateColumn({ type: 'timestamp', name: 'created_at' })
 | 
				
			||||||
 | 
					  created_at: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
 | 
				
			||||||
 | 
					  updated_at: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { ScrapInterface } from '../../scrap-interface';
 | 
				
			||||||
 | 
					import { ScrapModel } from '../../scrap-model';
 | 
				
			||||||
 | 
					import * as cheerio from 'cheerio';
 | 
				
			||||||
 | 
					import { Element } from 'domhandler';
 | 
				
			||||||
 | 
					import { ScrapItem } from '@/modules/scraps/entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { extractModelId, extractNumber } from '@/ultils';
 | 
				
			||||||
 | 
					export class GraysScrapModel extends ScrapModel {
 | 
				
			||||||
 | 
					  action = async () => {
 | 
				
			||||||
 | 
					    const urls = this.extractUrls();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log({ urls });
 | 
				
			||||||
 | 
					    const results = await Promise.allSettled(
 | 
				
			||||||
 | 
					      urls.map(async (item) => ({
 | 
				
			||||||
 | 
					        html: (await axios.get(item.url)).data,
 | 
				
			||||||
 | 
					        keyword: item.keyword,
 | 
				
			||||||
 | 
					      })),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const htmlsData = results
 | 
				
			||||||
 | 
					      .filter((res) => res.status === 'fulfilled')
 | 
				
			||||||
 | 
					      .map((res) => (res as PromiseFulfilledResult<any>).value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all(
 | 
				
			||||||
 | 
					      htmlsData.map(async (cur) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const data = await this.getItemsInHtml(cur);
 | 
				
			||||||
 | 
					          const results = this.filterItemByKeyword(cur.keyword, data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          this.results[cur.keyword] = results; // hoặc push như gợi ý trên
 | 
				
			||||||
 | 
					          return results;
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					          console.error(`❌ Error with keyword ${cur.keyword}:`, err);
 | 
				
			||||||
 | 
					          return []; // fallback
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getItemsInHtml = async ({
 | 
				
			||||||
 | 
					    html,
 | 
				
			||||||
 | 
					    keyword,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    html: string;
 | 
				
			||||||
 | 
					    keyword: string;
 | 
				
			||||||
 | 
					  }) => {
 | 
				
			||||||
 | 
					    const $ = cheerio.load(html);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const container = $('.sc-102aeaf3-1.eYPitT');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const items = container.children('div').toArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results = items.map((el) => {
 | 
				
			||||||
 | 
					      const url = $(el).find('.sc-pKqro.sc-gFnajm.gqkMpZ.dzWUkJ').attr('href');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        name: $(el).find('.sc-jlGgGc.dJRywx').text().trim(),
 | 
				
			||||||
 | 
					        image_url: $(el).find('img.sc-gtJxfw.jbgdlx').attr('src'),
 | 
				
			||||||
 | 
					        model: extractModelId(url),
 | 
				
			||||||
 | 
					        keyword,
 | 
				
			||||||
 | 
					        url,
 | 
				
			||||||
 | 
					        current_price: extractNumber(
 | 
				
			||||||
 | 
					          $(el).find('.sc-ijDOKB.sc-bStcSt.ikmQUw.eEycyP').text(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        scrap_config: { id: this.scrap_config_id },
 | 
				
			||||||
 | 
					      } as ScrapItem;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return results;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import { ScrapItem } from '../entities/scrap-item.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ScrapInterface {
 | 
				
			||||||
 | 
					  getItemsInHtml: (data: {
 | 
				
			||||||
 | 
					    html: string;
 | 
				
			||||||
 | 
					    keyword: string;
 | 
				
			||||||
 | 
					  }) => Promise<ScrapItem[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  action: () => Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					import { ScrapConfig } from '../entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import { ScrapItem } from '../entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { ScrapInterface } from './scrap-interface';
 | 
				
			||||||
 | 
					import { Element } from 'domhandler';
 | 
				
			||||||
 | 
					export class ScrapModel implements ScrapInterface {
 | 
				
			||||||
 | 
					  protected keywords: string;
 | 
				
			||||||
 | 
					  protected search_url: string;
 | 
				
			||||||
 | 
					  protected scrap_config_id: ScrapConfig['id'];
 | 
				
			||||||
 | 
					  public results: Record<string, ScrapItem[]> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor({
 | 
				
			||||||
 | 
					    keywords,
 | 
				
			||||||
 | 
					    search_url,
 | 
				
			||||||
 | 
					    scrap_config_id,
 | 
				
			||||||
 | 
					  }: {
 | 
				
			||||||
 | 
					    keywords: string;
 | 
				
			||||||
 | 
					    search_url: string;
 | 
				
			||||||
 | 
					    scrap_config_id: ScrapConfig['id'];
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    this.keywords = keywords;
 | 
				
			||||||
 | 
					    this.search_url = search_url;
 | 
				
			||||||
 | 
					    this.scrap_config_id = scrap_config_id;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected buildUrlWithKey(rawUrl: string, keyword: string) {
 | 
				
			||||||
 | 
					    return rawUrl.replaceAll('{{keyword}}', keyword);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected extractUrls(): { url: string; keyword: string }[] {
 | 
				
			||||||
 | 
					    // Tách từng key ra từ một chuỗi keywords
 | 
				
			||||||
 | 
					    const keywordList = this.keywords.split(', ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Dừng hàm nếu không có key nào
 | 
				
			||||||
 | 
					    if (keywordList.length <= 0) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Lập qua từng key để lấy url
 | 
				
			||||||
 | 
					    return keywordList.map((keyword) => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        url: this.buildUrlWithKey(this.search_url, keyword),
 | 
				
			||||||
 | 
					        keyword,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  action: () => Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getInfoItems: (
 | 
				
			||||||
 | 
					    data: { name: string; el: Element }[],
 | 
				
			||||||
 | 
					  ) => Record<string, string>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getItemsInHtml: (data: {
 | 
				
			||||||
 | 
					    html: string;
 | 
				
			||||||
 | 
					    keyword: string;
 | 
				
			||||||
 | 
					  }) => Promise<ScrapItem[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected filterItemByKeyword = (keyword: string, data: ScrapItem[]) => {
 | 
				
			||||||
 | 
					    return data.filter((item) =>
 | 
				
			||||||
 | 
					      item.name.toLowerCase().includes(keyword.toLowerCase()),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfig } from './entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import { ScrapItem } from './entities/scrap-item.entity';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from './services/scrap-config.service';
 | 
				
			||||||
 | 
					import { ScrapConfigsController } from './controllers/scrap-config.controller';
 | 
				
			||||||
 | 
					import { TasksService } from './services/tasks.service';
 | 
				
			||||||
 | 
					import { ScrapItemsService } from './services/scrap-item-config.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [TypeOrmModule.forFeature([ScrapConfig, ScrapItem])],
 | 
				
			||||||
 | 
					  providers: [ScrapConfigsService, TasksService, ScrapItemsService],
 | 
				
			||||||
 | 
					  exports: [ScrapConfigsService, TasksService, ScrapItemsService],
 | 
				
			||||||
 | 
					  controllers: [ScrapConfigsController],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ScrapsModule {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfig } from '../entities/scrap-config.entity';
 | 
				
			||||||
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
 | 
					import { CreateScrapConfigDto } from '../dto/scrap-config/create-scrap-config';
 | 
				
			||||||
 | 
					import { UpdateScrapConfigDto } from '../dto/scrap-config/update-scrap-config';
 | 
				
			||||||
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					import { WebBid } from '@/modules/bids/entities/wed-bid.entity';
 | 
				
			||||||
 | 
					import { GraysScrapModel } from '../models/https:/www.grays.com/grays-scrap-model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ScrapConfigsService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(ScrapConfig)
 | 
				
			||||||
 | 
					    readonly scrapConfigRepo: Repository<ScrapConfig>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async create(data: CreateScrapConfigDto) {
 | 
				
			||||||
 | 
					    const result = await this.scrapConfigRepo.save({
 | 
				
			||||||
 | 
					      search_url: data.search_url,
 | 
				
			||||||
 | 
					      keywords: data.keywords,
 | 
				
			||||||
 | 
					      web_bid: { id: data.web_id },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result) return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async update(
 | 
				
			||||||
 | 
					    id: ScrapConfig['id'],
 | 
				
			||||||
 | 
					    { web_id, ...data }: UpdateScrapConfigDto,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const result = await this.scrapConfigRepo.update(id, { ...data });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!result.affected) return AppResponse.toResponse(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  scrapModel(scrapConfig: ScrapConfig) {
 | 
				
			||||||
 | 
					    switch (scrapConfig.web_bid.origin_url) {
 | 
				
			||||||
 | 
					      case 'https://www.grays.com': {
 | 
				
			||||||
 | 
					        return new GraysScrapModel({
 | 
				
			||||||
 | 
					          ...scrapConfig,
 | 
				
			||||||
 | 
					          scrap_config_id: scrapConfig.id,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  scrapModels(data: ScrapConfig[]) {
 | 
				
			||||||
 | 
					    return data.map((item) => this.scrapModel(item)).filter((item) => !!item);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
 | 
					import { Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapItem } from '../entities/scrap-item.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class ScrapItemsService {
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @InjectRepository(ScrapItem)
 | 
				
			||||||
 | 
					    readonly scrapItemRepo: Repository<ScrapItem>,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
 | 
					import { IsNull, Not } from 'typeorm';
 | 
				
			||||||
 | 
					import { ScrapConfigsService } from './scrap-config.service';
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class TasksService {
 | 
				
			||||||
 | 
					  private readonly logger = new Logger(TasksService.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(private readonly scrapConfigsService: ScrapConfigsService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Cron(CronExpression.EVERY_MINUTE)
 | 
				
			||||||
 | 
					  async handleScraps() {
 | 
				
			||||||
 | 
					    // const scrapConfigs = await this.scrapConfigsService.scrapConfigRepo.find({
 | 
				
			||||||
 | 
					    //   where: {
 | 
				
			||||||
 | 
					    //     search_url: Not(IsNull()),
 | 
				
			||||||
 | 
					    //     keywords: Not(IsNull()),
 | 
				
			||||||
 | 
					    //   },
 | 
				
			||||||
 | 
					    //   relations: {
 | 
				
			||||||
 | 
					    //     web_bid: true,
 | 
				
			||||||
 | 
					    //   },
 | 
				
			||||||
 | 
					    // });
 | 
				
			||||||
 | 
					    // const models = this.scrapConfigsService.scrapModels(scrapConfigs);
 | 
				
			||||||
 | 
					    // await Promise.allSettled(models.map(async (item) => await item.action()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { Bid } from "@/modules/bids/entities/bid.entity";
 | 
					import { Bid } from '@/modules/bids/entities/bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function extractModelId(url: string): string | null {
 | 
					export function extractModelId(url: string): string | null {
 | 
				
			||||||
  switch (extractDomain(url)) {
 | 
					  switch (extractDomain(url)) {
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,10 @@ export function extractModelId(url: string): string | null {
 | 
				
			||||||
      const model = url.split('/').pop();
 | 
					      const model = url.split('/').pop();
 | 
				
			||||||
      return model ? model : null;
 | 
					      return model ? model : null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    case 'https://www.allbids.com.au': {
 | 
				
			||||||
 | 
					      const match = url.match(/-(\d+)(?:[\?#]|$)/);
 | 
				
			||||||
 | 
					      return match ? match[1] : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,71 +114,74 @@ export function verifyCode(content: string) {
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export function shouldResetTool(
 | 
					export function shouldResetTool(
 | 
				
			||||||
    bids: Bid[],
 | 
					  bids: Bid[],
 | 
				
			||||||
    lastResetTime: Date | null,
 | 
					  lastResetTime: Date | null,
 | 
				
			||||||
    now: Date = new Date(),
 | 
					  now: Date = new Date(),
 | 
				
			||||||
  ) {
 | 
					) {
 | 
				
			||||||
    const ONE_MINUTE = 60 * 1000;
 | 
					  const ONE_MINUTE = 60 * 1000;
 | 
				
			||||||
    const ONE_HOUR = 60 * ONE_MINUTE;
 | 
					  const ONE_HOUR = 60 * ONE_MINUTE;
 | 
				
			||||||
    const TWO_HOURS = 2 * ONE_HOUR;
 | 
					  const TWO_HOURS = 2 * ONE_HOUR;
 | 
				
			||||||
    const TWENTY_MINUTES = 20 * ONE_MINUTE;
 | 
					  const TWENTY_MINUTES = 20 * ONE_MINUTE;
 | 
				
			||||||
    const FIVE_MINUTES = 5 * ONE_MINUTE;
 | 
					  const FIVE_MINUTES = 5 * ONE_MINUTE;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Nếu đã reset trong 1 giờ gần đây => không reset
 | 
					 | 
				
			||||||
    if (lastResetTime && now.getTime() - lastResetTime.getTime() < ONE_HOUR) {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        shouldReset: false,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 1. Kiểm tra bid gần nhất có close_time trong vòng 20 phút tới
 | 
					 | 
				
			||||||
    const futureBids = bids
 | 
					 | 
				
			||||||
      .filter((b) => b.close_time)
 | 
					 | 
				
			||||||
      .map((b) => ({
 | 
					 | 
				
			||||||
        ...b,
 | 
					 | 
				
			||||||
        closeTime: new Date(b.close_time!),
 | 
					 | 
				
			||||||
      }))
 | 
					 | 
				
			||||||
      .filter((b) => b.closeTime.getTime() > now.getTime())
 | 
					 | 
				
			||||||
      .sort((a, b) => a.closeTime.getTime() - b.closeTime.getTime());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const closest = futureBids[0];
 | 
					 | 
				
			||||||
    const hasBidCloseSoon =
 | 
					 | 
				
			||||||
      closest && closest.closeTime.getTime() - now.getTime() <= TWENTY_MINUTES;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (hasBidCloseSoon) {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        shouldReset: true,
 | 
					 | 
				
			||||||
        reason: 'Bid close_time is within 20 minutes',
 | 
					 | 
				
			||||||
        bidId: closest.id,
 | 
					 | 
				
			||||||
        closeTime: closest.close_time
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 2. Kiểm tra bid chưa có lot_id hoặc close_time, được tạo > 5 phút và cách reset trước > 2 tiếng
 | 
					 | 
				
			||||||
    // Đồng thời không có bid nào sắp close trong 20 phút tới
 | 
					 | 
				
			||||||
    if (!hasBidCloseSoon) {
 | 
					 | 
				
			||||||
      for (const bid of bids) {
 | 
					 | 
				
			||||||
        const createdAt = new Date(bid.created_at);
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
          (!bid.lot_id || !bid.close_time) &&
 | 
					 | 
				
			||||||
          now.getTime() - createdAt.getTime() > FIVE_MINUTES &&
 | 
					 | 
				
			||||||
          (!lastResetTime ||
 | 
					 | 
				
			||||||
            now.getTime() - lastResetTime.getTime() > TWO_HOURS)
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            shouldReset: true,
 | 
					 | 
				
			||||||
            reason:
 | 
					 | 
				
			||||||
              'Bid is missing info and older than 5 mins, last reset > 2h, and no urgent bids',
 | 
					 | 
				
			||||||
            bidId: bid.id,
 | 
					 | 
				
			||||||
            closeTime: bid.close_time
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Nếu đã reset trong 1 giờ gần đây => không reset
 | 
				
			||||||
 | 
					  if (lastResetTime && now.getTime() - lastResetTime.getTime() < ONE_HOUR) {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      shouldReset: false,
 | 
					      shouldReset: false,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 1. Kiểm tra bid gần nhất có close_time trong vòng 20 phút tới
 | 
				
			||||||
 | 
					  const futureBids = bids
 | 
				
			||||||
 | 
					    .filter((b) => b.close_time)
 | 
				
			||||||
 | 
					    .map((b) => ({
 | 
				
			||||||
 | 
					      ...b,
 | 
				
			||||||
 | 
					      closeTime: new Date(b.close_time!),
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					    .filter((b) => b.closeTime.getTime() > now.getTime())
 | 
				
			||||||
 | 
					    .sort((a, b) => a.closeTime.getTime() - b.closeTime.getTime());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const closest = futureBids[0];
 | 
				
			||||||
 | 
					  const hasBidCloseSoon =
 | 
				
			||||||
 | 
					    closest && closest.closeTime.getTime() - now.getTime() <= TWENTY_MINUTES;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hasBidCloseSoon) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      shouldReset: true,
 | 
				
			||||||
 | 
					      reason: 'Bid close_time is within 20 minutes',
 | 
				
			||||||
 | 
					      bidId: closest.id,
 | 
				
			||||||
 | 
					      closeTime: closest.close_time,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 2. Kiểm tra bid chưa có lot_id hoặc close_time, được tạo > 5 phút và cách reset trước > 2 tiếng
 | 
				
			||||||
 | 
					  // Đồng thời không có bid nào sắp close trong 20 phút tới
 | 
				
			||||||
 | 
					  if (!hasBidCloseSoon) {
 | 
				
			||||||
 | 
					    for (const bid of bids) {
 | 
				
			||||||
 | 
					      const createdAt = new Date(bid.created_at);
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        (!bid.lot_id || !bid.close_time) &&
 | 
				
			||||||
 | 
					        now.getTime() - createdAt.getTime() > FIVE_MINUTES &&
 | 
				
			||||||
 | 
					        (!lastResetTime || now.getTime() - lastResetTime.getTime() > TWO_HOURS)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          shouldReset: true,
 | 
				
			||||||
 | 
					          reason:
 | 
				
			||||||
 | 
					            'Bid is missing info and older than 5 mins, last reset > 2h, and no urgent bids',
 | 
				
			||||||
 | 
					          bidId: bid.id,
 | 
				
			||||||
 | 
					          closeTime: bid.close_time,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    shouldReset: false,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractNumber(str: string) {
 | 
				
			||||||
 | 
					  const match = str.match(/\d+(\.\d+)?/);
 | 
				
			||||||
 | 
					  return match ? parseFloat(match[0]) : null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ import browser from "./system/browser.js";
 | 
				
			||||||
import configs from "./system/config.js";
 | 
					import configs from "./system/config.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  delay,
 | 
					  delay,
 | 
				
			||||||
  findNearestClosingChild,
 | 
					  extractModelId,
 | 
				
			||||||
  isTimeReached,
 | 
					  isTimeReached,
 | 
				
			||||||
  safeClosePage,
 | 
					  safeClosePage,
 | 
				
			||||||
  subtractSeconds,
 | 
					  subtractSeconds,
 | 
				
			||||||
| 
						 | 
					@ -274,21 +274,6 @@ const clearLazyTab = async () => {
 | 
				
			||||||
    // product tabs
 | 
					    // product tabs
 | 
				
			||||||
    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
					    const productTabs = _.flatMap(MANAGER_BIDS, "children");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // for (const item of [...productTabs, ...MANAGER_BIDS]) {
 | 
					 | 
				
			||||||
    //   if (!item.page_context) continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //   try {
 | 
					 | 
				
			||||||
    //     const avalableResult = await isPageAvailable(item.page_context);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //     if (!avalableResult) {
 | 
					 | 
				
			||||||
    //       await safeClosePage(item);
 | 
					 | 
				
			||||||
    //     }
 | 
					 | 
				
			||||||
    //   } catch (e) {
 | 
					 | 
				
			||||||
    //     console.warn("⚠️ Error checking page_context.title()", e.message);
 | 
					 | 
				
			||||||
    //     await safeClosePage(item);
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const page of pages) {
 | 
					    for (const page of pages) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
					        if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
 | 
				
			||||||
| 
						 | 
					@ -309,7 +294,7 @@ const clearLazyTab = async () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (!isTimeReached(earlyTrackingTime)) {
 | 
					          if (!isTimeReached(earlyTrackingTime)) {
 | 
				
			||||||
            await safeClosePage(productTab);
 | 
					            await safeClosePage(productTab);
 | 
				
			||||||
            console.log(`🛑 Unused page detected: ${pageUrl}`);
 | 
					            console.log(`🛑 Unused page detectedd: ${pageUrl}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            continue;
 | 
					            continue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
| 
						 | 
					@ -317,6 +302,22 @@ const clearLazyTab = async () => {
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const modelProductTab = extractModelId(pageUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (modelProductTab) {
 | 
				
			||||||
 | 
					          const productWatingUpdate = productTabs.find(
 | 
				
			||||||
 | 
					            (item) =>
 | 
				
			||||||
 | 
					              item.model === modelProductTab &&
 | 
				
			||||||
 | 
					              isTimeReached(item.close_time) &&
 | 
				
			||||||
 | 
					              item.status === "biding"
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (productWatingUpdate) {
 | 
				
			||||||
 | 
					            console.log("Waiting product update to close");
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // remove all listents
 | 
					        // remove all listents
 | 
				
			||||||
        page.removeAllListeners();
 | 
					        page.removeAllListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,175 @@
 | 
				
			||||||
 | 
					import fs from "fs";
 | 
				
			||||||
 | 
					import configs from "../../system/config.js";
 | 
				
			||||||
 | 
					import { getPathProfile, safeClosePage } from "../../system/utils.js";
 | 
				
			||||||
 | 
					import { ApiBid } from "../api-bid.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AllbidsApiBid extends ApiBid {
 | 
				
			||||||
 | 
					  reloadInterval;
 | 
				
			||||||
 | 
					  constructor({ ...prev }) {
 | 
				
			||||||
 | 
					    super(prev);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isLogin = async () => {
 | 
				
			||||||
 | 
					    if (!this.page_context) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentUrl = await this.page_context.url();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log({
 | 
				
			||||||
 | 
					      filePath,
 | 
				
			||||||
 | 
					      currentUrl,
 | 
				
			||||||
 | 
					      a: currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (currentUrl.includes(configs.WEB_URLS.ALLBIDS.LOGIN_URL)) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      !(await this.page_context.$('input[name="Username"]')) &&
 | 
				
			||||||
 | 
					      fs.existsSync(filePath)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleLogin() {
 | 
				
			||||||
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filePath = getPathProfile(this.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await page.waitForNavigation({ waitUntil: "domcontentloaded" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
				
			||||||
 | 
					    if (await this.isLogin()) {
 | 
				
			||||||
 | 
					      console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (fs.existsSync(filePath)) {
 | 
				
			||||||
 | 
					      console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
				
			||||||
 | 
					      fs.unlinkSync(filePath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const children = this.children.filter((item) => item.page_context);
 | 
				
			||||||
 | 
					    console.log(
 | 
				
			||||||
 | 
					      `🔍 [${this.id}] Found ${children.length} child pages to close.`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (children.length > 0) {
 | 
				
			||||||
 | 
					      console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
				
			||||||
 | 
					      await Promise.all(
 | 
				
			||||||
 | 
					        children.map((item) => {
 | 
				
			||||||
 | 
					          console.log(
 | 
				
			||||||
 | 
					            `➡ [${this.id}] Closing child page with context: ${item.page_context}`
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return safeClosePage(item);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `➡ [${this.id}] Closing main page context: ${this.page_context}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      await safeClosePage(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.onCloseLogin(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`🔑 [${this.id}] Starting login process...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // ⌨ Enter email
 | 
				
			||||||
 | 
					      console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
				
			||||||
 | 
					      await page.type('input[name="Username"]', this.username, {
 | 
				
			||||||
 | 
					        delay: 100,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ⌨ Enter password
 | 
				
			||||||
 | 
					      console.log(`✍ [${this.id}] Entering password...`);
 | 
				
			||||||
 | 
					      await page.type('input[name="Password"]', this.password, {
 | 
				
			||||||
 | 
					        delay: 150,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 🚀 Click the login button
 | 
				
			||||||
 | 
					      console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
				
			||||||
 | 
					      await page.click("#btnSignIn", { delay: 92 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // ⏳ Wait for navigation after login
 | 
				
			||||||
 | 
					      console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
				
			||||||
 | 
					      await page.waitForNavigation({
 | 
				
			||||||
 | 
					        timeout: 8000,
 | 
				
			||||||
 | 
					        waitUntil: "domcontentloaded",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(`🌍 [${this.id}] Current page after login:`, page.url());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 📂 Save session context to avoid re-login
 | 
				
			||||||
 | 
					      await this.saveContext();
 | 
				
			||||||
 | 
					      console.log(`✅ [${this.id}] Login successful!`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // await page.goto(this.url);
 | 
				
			||||||
 | 
					      console.log(`✅ [${this.id}] Navigation successful!`);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(
 | 
				
			||||||
 | 
					        `❌ [${this.id}] Error during login process:`,
 | 
				
			||||||
 | 
					        error.message
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  action = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      page.on("response", async (response) => {
 | 
				
			||||||
 | 
					        const request = response.request();
 | 
				
			||||||
 | 
					        if (request.redirectChain().length > 0) {
 | 
				
			||||||
 | 
					          if (response.url().includes(configs.WEB_CONFIGS.ALLBIDS.LOGIN_URL)) {
 | 
				
			||||||
 | 
					            await this.handleLogin();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await page.bringToFront();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Set userAgent
 | 
				
			||||||
 | 
					      await page.setUserAgent(
 | 
				
			||||||
 | 
					        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log("Error [action]: ", error.message);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  listen_events = async () => {
 | 
				
			||||||
 | 
					    if (this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results = await this.handlePrevListen();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!results) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.reloadInterval = setInterval(async () => {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (this.page_context && !this.page_context.isClosed()) {
 | 
				
			||||||
 | 
					          console.log(`🔄 [${this.id}] Reloading page...`);
 | 
				
			||||||
 | 
					          await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
				
			||||||
 | 
					          console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // this.handleUpdateWonItem();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          console.log(
 | 
				
			||||||
 | 
					            `❌ [${this.id}] Page context is closed. Stopping reload.`
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          clearInterval(this.reloadInterval);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, 60000); // 1p reload
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,290 @@
 | 
				
			||||||
 | 
					import _ from "lodash";
 | 
				
			||||||
 | 
					import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
 | 
				
			||||||
 | 
					import { sendMessage } from "../../system/apis/notification.js";
 | 
				
			||||||
 | 
					import { createOutBidLog } from "../../system/apis/out-bid-log.js";
 | 
				
			||||||
 | 
					import configs from "../../system/config.js";
 | 
				
			||||||
 | 
					import CONSTANTS from "../../system/constants.js";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  convertAETtoUTC,
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  removeFalsyValues,
 | 
				
			||||||
 | 
					  takeSnapshot,
 | 
				
			||||||
 | 
					} from "../../system/utils.js";
 | 
				
			||||||
 | 
					import { ProductBid } from "../product-bid.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AllbidsProductBid extends ProductBid {
 | 
				
			||||||
 | 
					  constructor({ ...prev }) {
 | 
				
			||||||
 | 
					    super(prev);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async waitForApiResponse() {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Chờ cho Angular load (có thể tùy chỉnh thời gian nếu cần)
 | 
				
			||||||
 | 
					      await this.page_context.waitForFunction(
 | 
				
			||||||
 | 
					        () => window.angular !== undefined
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const auctionData = await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					        let data = null;
 | 
				
			||||||
 | 
					        const elements = document.querySelectorAll(".ng-scope");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (let i = 0; i < elements.length; i++) {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            const scope = angular.element(elements[i]).scope();
 | 
				
			||||||
 | 
					            if (scope?.auction) {
 | 
				
			||||||
 | 
					              data = scope.auction;
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Thử lấy từ $parent nếu không thấy
 | 
				
			||||||
 | 
					            if (scope?.$parent?.auction) {
 | 
				
			||||||
 | 
					              data = scope.$parent.auction;
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            // Angular element có thể lỗi nếu phần tử không hợp lệ
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return auctionData;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `[${this.id}] Error in waitForApiResponse: ${error?.message}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleUpdateBid({
 | 
				
			||||||
 | 
					    lot_id,
 | 
				
			||||||
 | 
					    close_time,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    current_price,
 | 
				
			||||||
 | 
					    reserve_price,
 | 
				
			||||||
 | 
					    model,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    const response = await updateBid(this.id, {
 | 
				
			||||||
 | 
					      lot_id,
 | 
				
			||||||
 | 
					      close_time,
 | 
				
			||||||
 | 
					      name,
 | 
				
			||||||
 | 
					      current_price,
 | 
				
			||||||
 | 
					      reserve_price: Number(reserve_price) || 0,
 | 
				
			||||||
 | 
					      model,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response) {
 | 
				
			||||||
 | 
					      this.lot_id = response.lot_id;
 | 
				
			||||||
 | 
					      this.close_time = response.close_time;
 | 
				
			||||||
 | 
					      this.start_bid_time = response.start_bid_time;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async submitBid() {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await this.page_context.evaluate(
 | 
				
			||||||
 | 
					      async (aucID, bidAmount, submitUrl) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const url = `${submitUrl}?aucID=${aucID}&bidAmount=${bidAmount}&bidType=maximum`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const res = await fetch(url, {
 | 
				
			||||||
 | 
					            method: "POST",
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!res.ok) {
 | 
				
			||||||
 | 
					            return { success: false, message: `HTTP error ${res.status}` };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return data;
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          return { success: false, message: error.message || "Fetch failed" };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      this.model,
 | 
				
			||||||
 | 
					      this.max_price,
 | 
				
			||||||
 | 
					      configs.WEB_URLS.ALLBIDS.PLACE_BID
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return response;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update = async () => {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 📌 Chờ phản hồi API từ trang, tối đa 10 giây
 | 
				
			||||||
 | 
					    const result = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 📌 Nếu không có dữ liệu trả về thì dừng
 | 
				
			||||||
 | 
					    if (!result) {
 | 
				
			||||||
 | 
					      console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
 | 
				
			||||||
 | 
					    const data = removeFalsyValues(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        // model: result?.pid || null,
 | 
				
			||||||
 | 
					        lot_id: String(result?.aucCurrentBidID) || null,
 | 
				
			||||||
 | 
					        reserve_price: result?.aucBidIncrement || null,
 | 
				
			||||||
 | 
					        current_price: result.aucCurrentBid || null,
 | 
				
			||||||
 | 
					        close_time: result?.aucCloseUtc
 | 
				
			||||||
 | 
					          ? new Date(result.aucCloseUtc).toUTCString()
 | 
				
			||||||
 | 
					          : null,
 | 
				
			||||||
 | 
					        // close_time: close_time && !this.close_time ? String(close_time) : null, // test
 | 
				
			||||||
 | 
					        name: result?.aucTitle || null,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      ["close_time"]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`🚀 [${this.id}] Processed data ready for update`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 📌 Gửi dữ liệu cập nhật lên hệ thống
 | 
				
			||||||
 | 
					    await this.handleUpdateBid(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log("✅ Update successful!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { ...response, name: data.name, close_time: data.close_time };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handlePlaceBid() {
 | 
				
			||||||
 | 
					    if (!this.page_context) {
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `⚠️ [${this.id}] No page context found, aborting bid process.`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (global[`IS_PLACE_BID-${this.id}`]) {
 | 
				
			||||||
 | 
					      console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      console.log(`🔄 [${this.id}] Starting bid process...`);
 | 
				
			||||||
 | 
					      global[`IS_PLACE_BID-${this.id}`] = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Đợi phản hồi từ API
 | 
				
			||||||
 | 
					      const response = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        !response ||
 | 
				
			||||||
 | 
					        isTimeReached(new Date(response.aucCloseUtc).toUTCString())
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `⚠️ [${this.id}] Outbid detected, calling outBid function.`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        await outBid(this.id);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
 | 
				
			||||||
 | 
					      if (this.current_price > this.max_price + this.plus_price) {
 | 
				
			||||||
 | 
					        console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
 | 
				
			||||||
 | 
					        return; // Dừng hàm nếu giá đã vượt qua giới hạn
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Kiểm tra thời gian bid
 | 
				
			||||||
 | 
					      if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
 | 
				
			||||||
 | 
					            this.name || "None"
 | 
				
			||||||
 | 
					          }`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        !response ||
 | 
				
			||||||
 | 
					        (response?.aucUserMaxBid && response.aucUserMaxBid == this.max_price) ||
 | 
				
			||||||
 | 
					        response?.aucBidIncrement > this.max_price
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `⚠️ [${this.id}] No response or myBid equals max_price:`,
 | 
				
			||||||
 | 
					          response
 | 
				
			||||||
 | 
					        ); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
 | 
				
			||||||
 | 
					        return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const bidHistoriesItem = _.maxBy(this.histories, "price");
 | 
				
			||||||
 | 
					      console.log(`📜 [${this.id}] Current bid history:`, this.histories);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        bidHistoriesItem &&
 | 
				
			||||||
 | 
					        bidHistoriesItem?.price === this.current_price &&
 | 
				
			||||||
 | 
					        this.max_price == response?.aucUserMaxBid
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log("---------------------BIDDING--------------------");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const data = await this.submitBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log({ data });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.page_context.reload({ waitUntil: "networkidle0" });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { aucUserMaxBid } = await this.waitForApiResponse();
 | 
				
			||||||
 | 
					      console.log(`📡 [${this.id}] API Response received:`, lotData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 📌 Kiểm tra trạng thái đấu giá từ API
 | 
				
			||||||
 | 
					      if (aucUserMaxBid == this.max_price) {
 | 
				
			||||||
 | 
					        console.log(`📸 [${this.id}] Taking bid success snapshot...`);
 | 
				
			||||||
 | 
					        await takeSnapshot(
 | 
				
			||||||
 | 
					          page,
 | 
				
			||||||
 | 
					          this,
 | 
				
			||||||
 | 
					          "bid-success",
 | 
				
			||||||
 | 
					          CONSTANTS.TYPE_IMAGE.SUCCESS
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sendMessage(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pushPrice({
 | 
				
			||||||
 | 
					          bid_id: this.id,
 | 
				
			||||||
 | 
					          price: aucUserMaxBid,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`✅ [${this.id}] Bid placed successfully!`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        `⚠️ [${this.id}] Bid action completed, but status is still "None".`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      console.log(`🔚 [${this.id}] Resetting bid flag.`);
 | 
				
			||||||
 | 
					      global[`IS_PLACE_BID-${this.id}`] = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  action = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
 | 
				
			||||||
 | 
					      if (!page.url() || !page.url().includes(this.url)) {
 | 
				
			||||||
 | 
					        console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
 | 
				
			||||||
 | 
					        await this.gotoLink();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.handlePlaceBid();
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -339,6 +339,13 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isBided) {
 | 
					      if (isBided) {
 | 
				
			||||||
 | 
					        if (this.histories.length <= 0 && isTimeReached(this.start_bid_time)) {
 | 
				
			||||||
 | 
					          pushPrice({
 | 
				
			||||||
 | 
					            bid_id: this.id,
 | 
				
			||||||
 | 
					            price: this.max_price,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        console.log(`[${this.id}] This item bided. Skipping...`);
 | 
					        console.log(`[${this.id}] This item bided. Skipping...`);
 | 
				
			||||||
        global[`IS_PLACE_BID-${this.id}`] = false;
 | 
					        global[`IS_PLACE_BID-${this.id}`] = false;
 | 
				
			||||||
        global.IS_CLEANING = true;
 | 
					        global.IS_CLEANING = true;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,48 +3,54 @@
 | 
				
			||||||
// 1 : Apibids
 | 
					// 1 : Apibids
 | 
				
			||||||
// 2 : Producttab
 | 
					// 2 : Producttab
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { default: puppeteer } = require('puppeteer');
 | 
					const { default: puppeteer } = require("puppeteer");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Apibids = {
 | 
					Apibids = {
 | 
				
			||||||
    type: 'Apibid',
 | 
					  type: "Apibid",
 | 
				
			||||||
    puppeteer_connect: 'puppeteer_connect',
 | 
					  puppeteer_connect: "puppeteer_connect",
 | 
				
			||||||
    url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
 | 
					  url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
 | 
				
			||||||
    listentEvent: function () {
 | 
					  listentEvent: function () {
 | 
				
			||||||
        //action()
 | 
					    //action()
 | 
				
			||||||
    },
 | 
					  },
 | 
				
			||||||
    action: function () {},
 | 
					  action: function () {},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// n Producttab
 | 
					// n Producttab
 | 
				
			||||||
Producttab = {
 | 
					Producttab = {
 | 
				
			||||||
    type: 'Producttab',
 | 
					  type: "Producttab",
 | 
				
			||||||
    url: 'https://www.grays.com/mygrays/auctions/biddingon.aspx',
 | 
					  url: "https://www.grays.com/mygrays/auctions/biddingon.aspx",
 | 
				
			||||||
    puppeteer_connect: 'puppeteer_connect',
 | 
					  puppeteer_connect: "puppeteer_connect",
 | 
				
			||||||
    max_price: '',
 | 
					  max_price: "",
 | 
				
			||||||
    model: 'model',
 | 
					  model: "model",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    action: function () {},
 | 
					  action: function () {},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
manage = {
 | 
					manage = {
 | 
				
			||||||
    Apibids: { Apibids },
 | 
					  Apibids: { Apibids },
 | 
				
			||||||
    Producttabss: [{ Producttab }, { Producttab }],
 | 
					  Producttabss: [{ Producttab }, { Producttab }],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 10 goi 1 lan
 | 
					// 10 goi 1 lan
 | 
				
			||||||
recheck = function name() {
 | 
					recheck = function name() {
 | 
				
			||||||
    //check thay 2 record
 | 
					  //check thay 2 record
 | 
				
			||||||
    //tabApibids
 | 
					  //tabApibids
 | 
				
			||||||
    Apibids = new Apibids();
 | 
					  Apibids = new Apibids();
 | 
				
			||||||
    Apibids.puppeteer_connect();
 | 
					  Apibids.puppeteer_connect();
 | 
				
			||||||
    Apibids.listentEvent();
 | 
					  Apibids.listentEvent();
 | 
				
			||||||
    manage.Apibids = Apibids;
 | 
					  manage.Apibids = Apibids;
 | 
				
			||||||
    //n Producttab
 | 
					  //n Producttab
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
\*\* Tắt polling trước khi demo
 | 
					<!-- all bids -->
 | 
				
			||||||
 | 
					<!-- Lấy detail info của sản phẩm -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-   Trong thời gian đang bid nên mỡ tab lên -> hiện tại không mỡ tab lên khi start lại
 | 
					    let data = null; const elements = document.querySelectorAll('.ng-scope');
 | 
				
			||||||
-   Handle đăng nhập lại nếu không thành công -> hiện đang không đăng nhập lại nếu vì lí do nào đó không đăng nhập được
 | 
					    for (let i = 0; i < elements.length; i++) {
 | 
				
			||||||
-   Lịch sử bid đang có 2 lần trùng
 | 
					    const scope = angular.element(elements[i]).scope();
 | 
				
			||||||
 | 
					    if (scope && scope.auction) {
 | 
				
			||||||
 | 
					    console.log('Found at index:', i, 'Auction:', scope.auction); data = scope.auction;
 | 
				
			||||||
 | 
					    break; // dừng vòng lặp khi tìm thấy
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import * as fs from "fs";
 | 
					import * as fs from "fs";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
 | 
					import { AllbidsApiBid } from "../models/allbids.com.au/allbids-api-bid.js";
 | 
				
			||||||
 | 
					import { AllbidsProductBid } from "../models/allbids.com.au/allbids-product-bid.js";
 | 
				
			||||||
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
 | 
					import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
 | 
				
			||||||
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
 | 
					import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
 | 
				
			||||||
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
 | 
					import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
 | 
				
			||||||
| 
						 | 
					@ -39,6 +41,9 @@ export const createBidProduct = (web, data) => {
 | 
				
			||||||
    case configs.WEB_URLS.PICKLES: {
 | 
					    case configs.WEB_URLS.PICKLES: {
 | 
				
			||||||
      return new PicklesProductBid({ ...data });
 | 
					      return new PicklesProductBid({ ...data });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    case configs.WEB_URLS.ALLBIDS: {
 | 
				
			||||||
 | 
					      return new AllbidsProductBid({ ...data });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +61,9 @@ export const createApiBid = (web) => {
 | 
				
			||||||
    case configs.WEB_URLS.PICKLES: {
 | 
					    case configs.WEB_URLS.PICKLES: {
 | 
				
			||||||
      return new PicklesApiBid({ ...web });
 | 
					      return new PicklesApiBid({ ...web });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    case configs.WEB_URLS.ALLBIDS: {
 | 
				
			||||||
 | 
					      return new AllbidsApiBid({ ...web });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ const configs = {
 | 
				
			||||||
    LANGTONS: `https://www.langtons.com.au`,
 | 
					    LANGTONS: `https://www.langtons.com.au`,
 | 
				
			||||||
    LAWSONS: `https://www.lawsons.com.au`,
 | 
					    LAWSONS: `https://www.lawsons.com.au`,
 | 
				
			||||||
    PICKLES: `https://www.pickles.com.au`,
 | 
					    PICKLES: `https://www.pickles.com.au`,
 | 
				
			||||||
 | 
					    ALLBIDS: `https://www.allbids.com.au`,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  WEB_CONFIGS: {
 | 
					  WEB_CONFIGS: {
 | 
				
			||||||
    GRAYS: {
 | 
					    GRAYS: {
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,10 @@ const configs = {
 | 
				
			||||||
      API_CHECKOUT:
 | 
					      API_CHECKOUT:
 | 
				
			||||||
        "https://www.pickles.com.au/delegate/secured/bidding/confirm",
 | 
					        "https://www.pickles.com.au/delegate/secured/bidding/confirm",
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    ALLBIDS: {
 | 
				
			||||||
 | 
					      LOGIN_URL: "https://myaccount.allbids.com.au/account/login",
 | 
				
			||||||
 | 
					      PLACE_BID: "https://www.allbids.com.au/Bid/AjaxFinishBid",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -322,3 +322,43 @@ export function findNearestClosingChild(webBid) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return nearestChild || null;
 | 
					  return nearestChild || null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractModelId(url) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    switch (extractDomain(url)) {
 | 
				
			||||||
 | 
					      case "https://www.grays.com": {
 | 
				
			||||||
 | 
					        const match = url.match(/\/lot\/([\d-]+)\//);
 | 
				
			||||||
 | 
					        return match ? match[1] : null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "https://www.langtons.com.au": {
 | 
				
			||||||
 | 
					        const match = url.match(/auc-var-\d+/);
 | 
				
			||||||
 | 
					        return match[0];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "https://www.lawsons.com.au": {
 | 
				
			||||||
 | 
					        const match = url.split("_");
 | 
				
			||||||
 | 
					        return match ? match[1] : null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "https://www.pickles.com.au": {
 | 
				
			||||||
 | 
					        const model = url.split("/").pop();
 | 
				
			||||||
 | 
					        return model ? model : null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "https://www.allbids.com.au": {
 | 
				
			||||||
 | 
					        const match = url.match(/-(\d+)(?:[\?#]|$)/);
 | 
				
			||||||
 | 
					        return match ? match[1] : null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractDomain(url) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const parsedUrl = new URL(url);
 | 
				
			||||||
 | 
					    return parsedUrl.origin;
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -1,134 +0,0 @@
 | 
				
			||||||
/* #bid-extension body {
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  background-color: #121212;
 | 
					 | 
				
			||||||
  color: #e0e0e0;
 | 
					 | 
				
			||||||
  font-family: 'Segoe UI', Tahoma, sans-serif;
 | 
					 | 
				
			||||||
  width: 320px;
 | 
					 | 
				
			||||||
} */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension {
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  background-color: #121212;
 | 
					 | 
				
			||||||
  color: #e0e0e0;
 | 
					 | 
				
			||||||
  font-family: "Segoe UI", Tahoma, sans-serif;
 | 
					 | 
				
			||||||
  width: 320px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .container {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  gap: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension h2 {
 | 
					 | 
				
			||||||
  text-align: center;
 | 
					 | 
				
			||||||
  margin-bottom: 12px;
 | 
					 | 
				
			||||||
  font-size: 22px;
 | 
					 | 
				
			||||||
  color: #ffffff;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension label {
 | 
					 | 
				
			||||||
  font-size: 13px;
 | 
					 | 
				
			||||||
  margin-bottom: 2px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension input,
 | 
					 | 
				
			||||||
#bid-extension textarea {
 | 
					 | 
				
			||||||
  padding: 8px;
 | 
					 | 
				
			||||||
  background-color: #1e1e1e;
 | 
					 | 
				
			||||||
  color: #ffffff;
 | 
					 | 
				
			||||||
  border: 1px solid #333;
 | 
					 | 
				
			||||||
  border-radius: 6px;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension input:focus,
 | 
					 | 
				
			||||||
#bid-extension textarea:focus {
 | 
					 | 
				
			||||||
  border-color: #4a90e2;
 | 
					 | 
				
			||||||
  outline: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .row {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  gap: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .col {
 | 
					 | 
				
			||||||
  flex: 1;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .inputs .col {
 | 
					 | 
				
			||||||
  padding-left: 0px;
 | 
					 | 
				
			||||||
  padding-right: 0px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension button {
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
  background: linear-gradient(to right, #4a90e2, #357abd);
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  border-radius: 6px;
 | 
					 | 
				
			||||||
  font-size: 15px;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  transition: background 0.3s ease;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension button:hover {
 | 
					 | 
				
			||||||
  background: linear-gradient(to right, #3a78c2, #2d5faa);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension #errorMessage {
 | 
					 | 
				
			||||||
  margin-top: 2px;
 | 
					 | 
				
			||||||
  font-size: 11px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .wrapper {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .key-container {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 20px;
 | 
					 | 
				
			||||||
  left: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .key-container a {
 | 
					 | 
				
			||||||
  color: #ffffff;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  padding: 4px 10px;
 | 
					 | 
				
			||||||
  background: linear-gradient(to right, #4a90e2, #357abd);
 | 
					 | 
				
			||||||
  border-radius: 6px;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  transition: all 0.3s ease;
 | 
					 | 
				
			||||||
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .key-container a:hover {
 | 
					 | 
				
			||||||
  background: linear-gradient(to right, #3a78c2, #2d5faa);
 | 
					 | 
				
			||||||
  box-shadow: 0 6px 10px rgba(0, 0, 0, 0.3);
 | 
					 | 
				
			||||||
  transform: translateY(-2px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension .inputs {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  gap: 8px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#bid-extension svg {
 | 
					 | 
				
			||||||
  width: 14px;
 | 
					 | 
				
			||||||
  height: 14px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#toggle-bid-extension svg {
 | 
					 | 
				
			||||||
  width: 20px;
 | 
					 | 
				
			||||||
  height: 20px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 4.0 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 305 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 522 B  | 
| 
						 | 
					@ -1,4 +0,0 @@
 | 
				
			||||||
chrome.action.onClicked.addListener((tab) => {
 | 
					 | 
				
			||||||
  // Lấy URL của tab hiện tại
 | 
					 | 
				
			||||||
  console.log("Current URL:", tab.url);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +0,0 @@
 | 
				
			||||||
// config.js
 | 
					 | 
				
			||||||
const CONFIG = {
 | 
					 | 
				
			||||||
  API_BASE_URL: "http://localhost:4000/api/v1",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default CONFIG;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,394 +0,0 @@
 | 
				
			||||||
const CONFIG = {
 | 
					 | 
				
			||||||
  API_BASE_URL: "http://localhost:4000/api/v1",
 | 
					 | 
				
			||||||
  // API_BASE_URL: "https://bids.apactech.io/api/v1",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let PREV_DATA = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function removeFalsyValues(obj, excludeKeys = []) {
 | 
					 | 
				
			||||||
  return Object.entries(obj).reduce((acc, [key, value]) => {
 | 
					 | 
				
			||||||
    if (value || excludeKeys.includes(key)) {
 | 
					 | 
				
			||||||
      acc[key] = value;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return acc;
 | 
					 | 
				
			||||||
  }, {});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function extractDomain(url) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const parsedUrl = new URL(url);
 | 
					 | 
				
			||||||
    return parsedUrl.origin;
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    return null;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function extractModelId(url) {
 | 
					 | 
				
			||||||
  switch (extractDomain(url)) {
 | 
					 | 
				
			||||||
    case "https://www.grays.com": {
 | 
					 | 
				
			||||||
      const match = url.match(/\/lot\/([\d-]+)\//);
 | 
					 | 
				
			||||||
      return match ? match[1] : null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    case "https://www.langtons.com.au": {
 | 
					 | 
				
			||||||
      const match = url.match(/auc-var-\d+/);
 | 
					 | 
				
			||||||
      return match[0];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    case "https://www.lawsons.com.au": {
 | 
					 | 
				
			||||||
      const match = url.split("_");
 | 
					 | 
				
			||||||
      return match ? match[1] : null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    case "https://www.pickles.com.au": {
 | 
					 | 
				
			||||||
      const model = url.split("/").pop();
 | 
					 | 
				
			||||||
      return model ? model : null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showPage = async (pageLink = "pages/popup/popup.html") => {
 | 
					 | 
				
			||||||
  const res = await fetch(chrome.runtime.getURL(pageLink));
 | 
					 | 
				
			||||||
  const html = await res.text();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const wrapper = document.createElement("div");
 | 
					 | 
				
			||||||
  wrapper.innerHTML = html;
 | 
					 | 
				
			||||||
  document.body.appendChild(wrapper);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getKey = () => {
 | 
					 | 
				
			||||||
  return new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
    chrome.storage.local.get("key", (result) => {
 | 
					 | 
				
			||||||
      if (chrome.runtime.lastError) {
 | 
					 | 
				
			||||||
        reject(chrome.runtime.lastError);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        resolve(result.key || null);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handleCreate(event, formElements) {
 | 
					 | 
				
			||||||
  event.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const key = await getKey();
 | 
					 | 
				
			||||||
  if (!key) {
 | 
					 | 
				
			||||||
    showKey();
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const maxPrice = parseFloat(formElements.maxPrice.value);
 | 
					 | 
				
			||||||
  const plusPrice = parseFloat(formElements.plusPrice.value);
 | 
					 | 
				
			||||||
  const quantity = parseInt(formElements.quantity.value, 10);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const payload = {
 | 
					 | 
				
			||||||
    url: formElements.url.value.trim(),
 | 
					 | 
				
			||||||
    max_price: isNaN(maxPrice) ? null : maxPrice,
 | 
					 | 
				
			||||||
    plus_price: isNaN(plusPrice) ? null : plusPrice,
 | 
					 | 
				
			||||||
    quantity: isNaN(quantity) ? null : quantity,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Validate required fields
 | 
					 | 
				
			||||||
  if (!payload.url || payload.max_price === null) {
 | 
					 | 
				
			||||||
    alert("Please fill out the URL and Max Price fields correctly.");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await fetch(`${CONFIG.API_BASE_URL}/bids`, {
 | 
					 | 
				
			||||||
      method: "POST",
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
        Authorization: key,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      body: JSON.stringify(removeFalsyValues(payload)),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const result = await response.json();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    alert(result.message);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // showInfo
 | 
					 | 
				
			||||||
    await showInfo(extractModelId(payload.url), formElements);
 | 
					 | 
				
			||||||
    // handleChangeTitleButton
 | 
					 | 
				
			||||||
    handleChangeTitleButton(true, formElements);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    alert("Error: " + error.message);
 | 
					 | 
				
			||||||
    console.error("API Error:", error);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function handleUpdate(event, formElements, id) {
 | 
					 | 
				
			||||||
  event.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const key = await getKey();
 | 
					 | 
				
			||||||
  if (!key) {
 | 
					 | 
				
			||||||
    showKey();
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const maxPrice = parseFloat(formElements.maxPrice.value);
 | 
					 | 
				
			||||||
  const plusPrice = parseFloat(formElements.plusPrice.value);
 | 
					 | 
				
			||||||
  const quantity = parseInt(formElements.quantity.value, 10);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const payload = {
 | 
					 | 
				
			||||||
    max_price: isNaN(maxPrice) ? null : maxPrice,
 | 
					 | 
				
			||||||
    plus_price: isNaN(plusPrice) ? null : plusPrice,
 | 
					 | 
				
			||||||
    quantity: isNaN(quantity) ? null : quantity,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Validate required fields
 | 
					 | 
				
			||||||
  if (payload.max_price === null) {
 | 
					 | 
				
			||||||
    alert("Please fill out the URL and Max Price fields correctly.");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await fetch(`${CONFIG.API_BASE_URL}/bids/info/${id}`, {
 | 
					 | 
				
			||||||
      method: "PUT",
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
        Authorization: key,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      body: JSON.stringify(removeFalsyValues(payload)),
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const result = await response.json();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    alert(result.message);
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    alert("Error: " + error.message);
 | 
					 | 
				
			||||||
    console.error("API Error:", error);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showBid = () => {
 | 
					 | 
				
			||||||
  const formKey = document.getElementById("form-key");
 | 
					 | 
				
			||||||
  const formBid = document.getElementById("form-bid");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  formKey.style.display = "none";
 | 
					 | 
				
			||||||
  formBid.style.display = "block";
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showKey = async () => {
 | 
					 | 
				
			||||||
  const key = await getKey();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const formKey = document.getElementById("form-key");
 | 
					 | 
				
			||||||
  const formBid = document.getElementById("form-bid");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const keyEl = document.querySelector("#form-key #key");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  formBid.style.display = "none";
 | 
					 | 
				
			||||||
  formKey.style.display = "block";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (key && keyEl) {
 | 
					 | 
				
			||||||
    keyEl.value = key;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const handleToogle = async () => {
 | 
					 | 
				
			||||||
  const btn = document.getElementById("toggle-bid-extension");
 | 
					 | 
				
			||||||
  const panel = document.getElementById("bid-extension");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Kiểm tra xem nút và panel có tồn tại hay không
 | 
					 | 
				
			||||||
  if (btn && panel) {
 | 
					 | 
				
			||||||
    btn.addEventListener("click", async () => {
 | 
					 | 
				
			||||||
      panel.style.display = panel.style.display === "none" ? "block" : "none";
 | 
					 | 
				
			||||||
      await handleShowForm();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    console.error("Không tìm thấy nút hoặc panel!");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const handleShowForm = async () => {
 | 
					 | 
				
			||||||
  const toggleBtn = document.getElementById("toggle-bid-extension");
 | 
					 | 
				
			||||||
  const formBid = document.getElementById("form-bid");
 | 
					 | 
				
			||||||
  const formKey = document.getElementById("form-key");
 | 
					 | 
				
			||||||
  const keyBtn = document.getElementById("key-btn");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const isVisible = (el) => el && el.style.display !== "none";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Toggle hiển thị form hiện tại (bid hoặc key)
 | 
					 | 
				
			||||||
  toggleBtn?.addEventListener("click", async () => {
 | 
					 | 
				
			||||||
    if (isVisible(formBid)) {
 | 
					 | 
				
			||||||
      formBid.style.display = "none";
 | 
					 | 
				
			||||||
    } else if (isVisible(formKey)) {
 | 
					 | 
				
			||||||
      formKey.style.display = "none";
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const currentKey = await getKey();
 | 
					 | 
				
			||||||
      if (!currentKey) {
 | 
					 | 
				
			||||||
        showKey();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        showBid();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Nhấn vào icon key để chuyển sang form-key
 | 
					 | 
				
			||||||
  keyBtn?.addEventListener("click", () => {
 | 
					 | 
				
			||||||
    showKey();
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const handleChangeTitleButton = (result, formElements) => {
 | 
					 | 
				
			||||||
  if (result) {
 | 
					 | 
				
			||||||
    formElements.createBtn.textContent = "Update";
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    formElements.createBtn.textContent = "Create";
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const handleSaveKey = () => {
 | 
					 | 
				
			||||||
  const form = document.querySelector("#form-key form");
 | 
					 | 
				
			||||||
  if (!form) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  form.addEventListener("submit", async (e) => {
 | 
					 | 
				
			||||||
    e.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const inputKey = form.querySelector("#key");
 | 
					 | 
				
			||||||
    if (!inputKey) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const keyValue = inputKey.value.trim();
 | 
					 | 
				
			||||||
    if (!keyValue) {
 | 
					 | 
				
			||||||
      alert("Please enter a key");
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Lưu vào chrome.storage.local
 | 
					 | 
				
			||||||
    chrome.storage.local.set({ key: keyValue }, async () => {
 | 
					 | 
				
			||||||
      alert("Key saved successfully!");
 | 
					 | 
				
			||||||
      showBid();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!isValidModel()) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await showInfo();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isValidModel = () => {
 | 
					 | 
				
			||||||
  const currentUrl = window.location.href;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const model = extractModelId(currentUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return !!model;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const createInfoColumn = (data, formElements) => {
 | 
					 | 
				
			||||||
  const inputsContainer = document.querySelector("#bid-extension .inputs");
 | 
					 | 
				
			||||||
  const urlCol = document.querySelector("#url-col");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!inputsContainer || !urlCol) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 1. Thêm ID và Name vào đầu inputsContainer
 | 
					 | 
				
			||||||
  const otherEls = `
 | 
					 | 
				
			||||||
    <div class="col">
 | 
					 | 
				
			||||||
      <label>ID</label>
 | 
					 | 
				
			||||||
      <input readonly value="${data?.id || "None"}" type="text" id="id" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="col">
 | 
					 | 
				
			||||||
      <label>Name</label>
 | 
					 | 
				
			||||||
      <textarea readonly id="maxPrice">${data?.name || "None"}</textarea>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  `;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  inputsContainer.insertAdjacentHTML("afterbegin", otherEls);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // 2. Tạo và chèn Current Price ngay sau #url-col
 | 
					 | 
				
			||||||
  const currentPriceDiv = document.createElement("div");
 | 
					 | 
				
			||||||
  currentPriceDiv.className = "col";
 | 
					 | 
				
			||||||
  currentPriceDiv.innerHTML = `
 | 
					 | 
				
			||||||
    <label>Current price</label>
 | 
					 | 
				
			||||||
    <input readonly type="text" value="${
 | 
					 | 
				
			||||||
      data?.current_price || "None"
 | 
					 | 
				
			||||||
    }" id="currentPrice" />
 | 
					 | 
				
			||||||
  `;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  urlCol.parentNode.insertBefore(currentPriceDiv, urlCol.nextSibling);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  formElements.quantity.value = data?.quantity || 1;
 | 
					 | 
				
			||||||
  formElements.plusPrice.value = data?.plus_price || 0;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showInfo = async (model, formElements) => {
 | 
					 | 
				
			||||||
  const key = await getKey();
 | 
					 | 
				
			||||||
  if (!key) {
 | 
					 | 
				
			||||||
    showKey();
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    const response = await fetch(`${CONFIG.API_BASE_URL}/bids/${model}`, {
 | 
					 | 
				
			||||||
      method: "GET",
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
        Authorization: key,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const result = await response.json();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!result || result?.status_code !== 200 || !result?.data) {
 | 
					 | 
				
			||||||
      if (result.status_code !== 404) {
 | 
					 | 
				
			||||||
        alert(result.message);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      PREV_DATA = null;
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    formElements.maxPrice.value = result.data.max_price;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    createInfoColumn(result.data, formElements);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    PREV_DATA = result;
 | 
					 | 
				
			||||||
    return result;
 | 
					 | 
				
			||||||
  } catch (error) {
 | 
					 | 
				
			||||||
    alert("Error: " + error.message);
 | 
					 | 
				
			||||||
    console.error("API Error:", error);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
(async () => {
 | 
					 | 
				
			||||||
  await showPage();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const formElements = {
 | 
					 | 
				
			||||||
    url: document.querySelector("#form-bid #url"),
 | 
					 | 
				
			||||||
    maxPrice: document.querySelector("#form-bid #maxPrice"),
 | 
					 | 
				
			||||||
    plusPrice: document.querySelector("#form-bid #plusPrice"),
 | 
					 | 
				
			||||||
    quantity: document.querySelector("#form-bid #quantity"),
 | 
					 | 
				
			||||||
    createBtn: document.querySelector("#form-bid #createBtn"),
 | 
					 | 
				
			||||||
    form: document.querySelector("#form-bid form"),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const style = document.createElement("link");
 | 
					 | 
				
			||||||
  style.rel = "stylesheet";
 | 
					 | 
				
			||||||
  style.href = chrome.runtime.getURL("assets/css/index.css");
 | 
					 | 
				
			||||||
  document.head.appendChild(style);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const script = document.createElement("script");
 | 
					 | 
				
			||||||
  script.type = "module";
 | 
					 | 
				
			||||||
  script.src = chrome.runtime.getURL("pages/popup/popup.js");
 | 
					 | 
				
			||||||
  script.defer = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  document.body.appendChild(script);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleSaveKey();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const currentUrl = window.location.href;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const model = extractModelId(currentUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!model) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // set url on form
 | 
					 | 
				
			||||||
  formElements.url.value = currentUrl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  await showInfo(model, formElements);
 | 
					 | 
				
			||||||
  handleChangeTitleButton(!!PREV_DATA, formElements);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  formElements.form.addEventListener("submit", (e) =>
 | 
					 | 
				
			||||||
    PREV_DATA
 | 
					 | 
				
			||||||
      ? handleUpdate(e, formElements, PREV_DATA.data.id)
 | 
					 | 
				
			||||||
      : handleCreate(e, formElements)
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
})();
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,42 +0,0 @@
 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  "manifest_version": 3,
 | 
					 | 
				
			||||||
  "name": "Bid Extension",
 | 
					 | 
				
			||||||
  "version": "1.0",
 | 
					 | 
				
			||||||
  "description": "Bid Extension",
 | 
					 | 
				
			||||||
  "action": {
 | 
					 | 
				
			||||||
    "default_popup": "pages/popup/popup.html",
 | 
					 | 
				
			||||||
    "default_icon": {
 | 
					 | 
				
			||||||
      "16": "assets/icons/16.png",
 | 
					 | 
				
			||||||
      "32": "assets/icons/32.png",
 | 
					 | 
				
			||||||
      "128": "assets/icons/128.png"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "background": {
 | 
					 | 
				
			||||||
    "service_worker": "background.js"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "permissions": ["storage"],
 | 
					 | 
				
			||||||
  "host_permissions": ["http://*/*", "https://*/*"],
 | 
					 | 
				
			||||||
  "content_scripts": [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "matches": ["<all_urls>"],
 | 
					 | 
				
			||||||
      "js": ["content.js"]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  "web_accessible_resources": [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "resources": [
 | 
					 | 
				
			||||||
        "pages/popup/popup.html",
 | 
					 | 
				
			||||||
        "pages/popup/popup.js",
 | 
					 | 
				
			||||||
        "assets/css/index.css",
 | 
					 | 
				
			||||||
        "config.js",
 | 
					 | 
				
			||||||
        "assets/icons/*"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "matches": ["<all_urls>"]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
  "icons": {
 | 
					 | 
				
			||||||
    "16": "assets/icons/16.png",
 | 
					 | 
				
			||||||
    "32": "assets/icons/32.png",
 | 
					 | 
				
			||||||
    "128": "assets/icons/128.png"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,170 +0,0 @@
 | 
				
			||||||
<div
 | 
					 | 
				
			||||||
  id="bid-toggle-container"
 | 
					 | 
				
			||||||
  style="position: fixed; bottom: 20px; left: 20px; z-index: 9999"
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
  <button
 | 
					 | 
				
			||||||
    id="toggle-bid-extension"
 | 
					 | 
				
			||||||
    style="
 | 
					 | 
				
			||||||
      padding: 12px 20px;
 | 
					 | 
				
			||||||
      background: #2c2f36;
 | 
					 | 
				
			||||||
      color: #ffffff;
 | 
					 | 
				
			||||||
      border: none;
 | 
					 | 
				
			||||||
      border-radius: 9999px;
 | 
					 | 
				
			||||||
      font-size: 15px;
 | 
					 | 
				
			||||||
      font-weight: 500;
 | 
					 | 
				
			||||||
      cursor: pointer;
 | 
					 | 
				
			||||||
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
 | 
					 | 
				
			||||||
      transition: background 0.3s ease, transform 0.2s ease;
 | 
					 | 
				
			||||||
    "
 | 
					 | 
				
			||||||
    onmouseover="this.style.background='#3a3d44'; this.style.transform='scale(1.05)'"
 | 
					 | 
				
			||||||
    onmouseout="this.style.background='#2c2f36'; this.style.transform='scale(1)'"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    <svg
 | 
					 | 
				
			||||||
      fill="#ffffff"
 | 
					 | 
				
			||||||
      version="1.1"
 | 
					 | 
				
			||||||
      id="Capa_1"
 | 
					 | 
				
			||||||
      xmlns="http://www.w3.org/2000/svg"
 | 
					 | 
				
			||||||
      xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
					 | 
				
			||||||
      x="0px"
 | 
					 | 
				
			||||||
      y="0px"
 | 
					 | 
				
			||||||
      width="20px"
 | 
					 | 
				
			||||||
      height="20px"
 | 
					 | 
				
			||||||
      viewBox="0 0 494.212 494.212"
 | 
					 | 
				
			||||||
      style="enable-background: new 0 0 494.212 494.212"
 | 
					 | 
				
			||||||
      xml:space="preserve"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <g>
 | 
					 | 
				
			||||||
        <path
 | 
					 | 
				
			||||||
          d="M483.627,401.147L379.99,297.511c-7.416-7.043-16.084-10.567-25.981-10.567c-10.088,0-19.222,4.093-27.401,12.278
 | 
					 | 
				
			||||||
   l-73.087-73.087l35.98-35.976c2.663-2.667,3.997-5.901,3.997-9.71c0-3.806-1.334-7.042-3.997-9.707
 | 
					 | 
				
			||||||
   c0.377,0.381,1.52,1.569,3.423,3.571c1.902,2,3.142,3.188,3.72,3.571c0.571,0.378,1.663,1.328,3.278,2.853
 | 
					 | 
				
			||||||
   c1.625,1.521,2.901,2.475,3.856,2.853c0.958,0.378,2.245,0.95,3.867,1.713c1.615,0.761,3.183,1.283,4.709,1.57
 | 
					 | 
				
			||||||
   c1.522,0.284,3.237,0.428,5.14,0.428c7.228,0,13.703-2.665,19.411-7.995c0.574-0.571,2.286-2.14,5.14-4.712
 | 
					 | 
				
			||||||
   c2.861-2.574,4.805-4.377,5.855-5.426c1.047-1.047,2.621-2.806,4.716-5.28c2.091-2.475,3.569-4.57,4.425-6.283
 | 
					 | 
				
			||||||
   c0.853-1.711,1.708-3.806,2.57-6.28c0.855-2.474,1.279-4.949,1.279-7.423c0-7.614-2.665-14.087-7.994-19.417L236.41,8.003
 | 
					 | 
				
			||||||
   c-5.33-5.33-11.802-7.994-19.413-7.994c-2.474,0-4.948,0.428-7.426,1.283c-2.475,0.854-4.567,1.713-6.28,2.568
 | 
					 | 
				
			||||||
   c-1.714,0.855-3.806,2.331-6.28,4.427c-2.474,2.094-4.233,3.665-5.282,4.712c-1.047,1.049-2.855,3-5.424,5.852
 | 
					 | 
				
			||||||
   c-2.572,2.856-4.143,4.57-4.712,5.142c-5.327,5.708-7.994,12.181-7.994,19.414c0,1.903,0.144,3.616,0.431,5.137
 | 
					 | 
				
			||||||
   c0.288,1.525,0.809,3.094,1.571,4.714c0.76,1.618,1.331,2.903,1.713,3.853c0.378,0.95,1.328,2.24,2.852,3.858
 | 
					 | 
				
			||||||
   c1.525,1.615,2.475,2.712,2.856,3.284c0.378,0.575,1.571,1.809,3.567,3.715c2,1.902,3.193,3.049,3.571,3.427
 | 
					 | 
				
			||||||
   c-2.664-2.667-5.901-3.999-9.707-3.999s-7.043,1.331-9.707,3.999l-99.371,99.357c-2.667,2.666-3.999,5.901-3.999,9.707
 | 
					 | 
				
			||||||
   c0,3.809,1.331,7.045,3.999,9.71c-0.381-0.381-1.524-1.574-3.427-3.571c-1.902-2-3.14-3.189-3.711-3.571
 | 
					 | 
				
			||||||
   c-0.571-0.378-1.665-1.328-3.283-2.852c-1.619-1.521-2.905-2.474-3.855-2.853c-0.95-0.378-2.235-0.95-3.854-1.714
 | 
					 | 
				
			||||||
   c-1.615-0.76-3.186-1.282-4.71-1.569c-1.521-0.284-3.234-0.428-5.137-0.428c-7.233,0-13.709,2.664-19.417,7.994
 | 
					 | 
				
			||||||
   c-0.568,0.57-2.284,2.144-5.138,4.712c-2.856,2.572-4.803,4.377-5.852,5.426c-1.047,1.047-2.615,2.806-4.709,5.281
 | 
					 | 
				
			||||||
   c-2.093,2.474-3.571,4.568-4.426,6.283c-0.856,1.709-1.709,3.806-2.568,6.28C0.432,212.061,0,214.535,0,217.01
 | 
					 | 
				
			||||||
   c0,7.614,2.665,14.082,7.994,19.414l116.485,116.481c5.33,5.328,11.803,7.991,19.414,7.991c2.474,0,4.948-0.422,7.426-1.277
 | 
					 | 
				
			||||||
   c2.475-0.855,4.567-1.714,6.28-2.569c1.713-0.855,3.806-2.327,6.28-4.425s4.233-3.665,5.28-4.716
 | 
					 | 
				
			||||||
   c1.049-1.051,2.856-2.995,5.426-5.855c2.572-2.851,4.141-4.565,4.712-5.14c5.327-5.709,7.994-12.184,7.994-19.411
 | 
					 | 
				
			||||||
   c0-1.902-0.144-3.617-0.431-5.14c-0.288-1.526-0.809-3.094-1.571-4.716c-0.76-1.615-1.331-2.902-1.713-3.854
 | 
					 | 
				
			||||||
   c-0.378-0.951-1.328-2.238-2.852-3.86c-1.525-1.615-2.475-2.71-2.856-3.285c-0.38-0.571-1.571-1.807-3.567-3.717
 | 
					 | 
				
			||||||
   c-2.002-1.902-3.193-3.045-3.571-3.429c2.663,2.669,5.902,4.001,9.707,4.001c3.806,0,7.043-1.332,9.707-4.001l35.976-35.974
 | 
					 | 
				
			||||||
   l73.086,73.087c-8.186,8.186-12.278,17.312-12.278,27.401c0,10.283,3.621,18.843,10.849,25.7L401.42,483.643
 | 
					 | 
				
			||||||
   c7.042,7.035,15.604,10.561,25.693,10.561c9.896,0,18.555-3.525,25.981-10.561l30.546-30.841
 | 
					 | 
				
			||||||
   c7.043-7.043,10.571-15.605,10.571-25.693C494.212,417.231,490.684,408.566,483.627,401.147z"
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
      <g></g>
 | 
					 | 
				
			||||||
    </svg>
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
  </button>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div
 | 
					 | 
				
			||||||
  id="bid-extension"
 | 
					 | 
				
			||||||
  class="wrapper"
 | 
					 | 
				
			||||||
  style="
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
    position: fixed;
 | 
					 | 
				
			||||||
    bottom: 90px;
 | 
					 | 
				
			||||||
    left: 20px;
 | 
					 | 
				
			||||||
    z-index: 9999;
 | 
					 | 
				
			||||||
    background-color: #1e1e1e;
 | 
					 | 
				
			||||||
    border-radius: 12px;
 | 
					 | 
				
			||||||
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
 | 
					 | 
				
			||||||
    padding: 20px;
 | 
					 | 
				
			||||||
    width: 320px;
 | 
					 | 
				
			||||||
  "
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
  <!-- Form bid -->
 | 
					 | 
				
			||||||
  <div style="display: none" id="form-bid">
 | 
					 | 
				
			||||||
    <form class="container">
 | 
					 | 
				
			||||||
      <h2>Bid</h2>
 | 
					 | 
				
			||||||
      <div class="inputs">
 | 
					 | 
				
			||||||
        <div id="url-col" class="col">
 | 
					 | 
				
			||||||
          <label>Url</label>
 | 
					 | 
				
			||||||
          <input readonly type="text" id="url" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="col">
 | 
					 | 
				
			||||||
          <label>Max price</label>
 | 
					 | 
				
			||||||
          <input type="number" id="maxPrice" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="col">
 | 
					 | 
				
			||||||
          <label>Plus price</label>
 | 
					 | 
				
			||||||
          <input type="number" id="plusPrice" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="col">
 | 
					 | 
				
			||||||
          <label>Quantity</label>
 | 
					 | 
				
			||||||
          <input type="number" id="quantity" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <button type="submit" id="createBtn">Create</button>
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="key-container">
 | 
					 | 
				
			||||||
      <span id="key-btn" class="key-btn">
 | 
					 | 
				
			||||||
        <svg
 | 
					 | 
				
			||||||
          fill="#ffffff"
 | 
					 | 
				
			||||||
          height="14px"
 | 
					 | 
				
			||||||
          width="14px"
 | 
					 | 
				
			||||||
          viewBox="0 0 367.578 367.578"
 | 
					 | 
				
			||||||
          xmlns="http://www.w3.org/2000/svg"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <path
 | 
					 | 
				
			||||||
            d="M281.541,97.751c0-53.9-43.851-97.751-97.751-97.751S86.038,43.851,86.038,97.751
 | 
					 | 
				
			||||||
            c0,44.799,30.294,82.652,71.472,94.159v144.668c0,4.026,1.977,9.1,4.701,12.065l14.514,15.798
 | 
					 | 
				
			||||||
            c1.832,1.993,4.406,3.136,7.065,3.136s5.233-1.143,7.065-3.136l14.514-15.798
 | 
					 | 
				
			||||||
            c2.724-2.965,4.701-8.039,4.701-12.065v-7.387l14.592-9.363
 | 
					 | 
				
			||||||
            c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
 | 
					 | 
				
			||||||
            l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.909c0-2.744-1.471-5.262-4.036-6.907l-14.591-9.363v-0.207
 | 
					 | 
				
			||||||
            l14.592-9.363c2.564-1.646,4.035-4.164,4.035-6.908c0-2.745-1.471-5.263-4.036-6.909l-14.591-9.363V191.91
 | 
					 | 
				
			||||||
            C251.246,180.403,281.541,142.551,281.541,97.751z
 | 
					 | 
				
			||||||
            M183.789,104.948c-20.985,0-37.996-17.012-37.996-37.996s17.012-37.996,37.996-37.996
 | 
					 | 
				
			||||||
            s37.996,17.012,37.996,37.996S204.774,104.948,183.789,104.948z"
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </svg>
 | 
					 | 
				
			||||||
      </span>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <!-- Form key -->
 | 
					 | 
				
			||||||
  <div style="display: block" id="form-key">
 | 
					 | 
				
			||||||
    <form class="container">
 | 
					 | 
				
			||||||
      <h2>Key</h2>
 | 
					 | 
				
			||||||
      <div class="inputs">
 | 
					 | 
				
			||||||
        <div class="col">
 | 
					 | 
				
			||||||
          <label>Key</label>
 | 
					 | 
				
			||||||
          <input type="password" id="key" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <button type="submit" id="saveKeyBtn">Save</button>
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,17 +0,0 @@
 | 
				
			||||||
const handleToogle = () => {
 | 
					 | 
				
			||||||
  const btn = document.getElementById("toggle-bid-extension");
 | 
					 | 
				
			||||||
  const panel = document.getElementById("bid-extension");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Kiểm tra xem nút và panel có tồn tại hay không
 | 
					 | 
				
			||||||
  if (btn && panel) {
 | 
					 | 
				
			||||||
    btn.addEventListener("click", () => {
 | 
					 | 
				
			||||||
      panel.style.display = panel.style.display === "none" ? "block" : "none";
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    console.error("Không tìm thấy nút hoặc panel!");
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
handleToogle();
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue