Deploy to production #44
			
				
			
		
		
		
	| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
{"createdAt":1747191706164}
 | 
					{"createdAt":1747292824357}
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,7 @@
 | 
				
			||||||
        "dayjs": "^1.11.13",
 | 
					        "dayjs": "^1.11.13",
 | 
				
			||||||
        "imap": "^0.8.19",
 | 
					        "imap": "^0.8.19",
 | 
				
			||||||
        "lodash": "^4.17.21",
 | 
					        "lodash": "^4.17.21",
 | 
				
			||||||
 | 
					        "moment": "^2.30.1",
 | 
				
			||||||
        "multer": "^1.4.5-lts.1",
 | 
					        "multer": "^1.4.5-lts.1",
 | 
				
			||||||
        "mysql2": "^3.13.0",
 | 
					        "mysql2": "^3.13.0",
 | 
				
			||||||
        "nestjs-paginate": "^11.1.0",
 | 
					        "nestjs-paginate": "^11.1.0",
 | 
				
			||||||
| 
						 | 
					@ -8660,9 +8661,10 @@
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/moment": {
 | 
					    "node_modules/moment": {
 | 
				
			||||||
      "version": "2.8.3",
 | 
					      "version": "2.30.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==",
 | 
					      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": "*"
 | 
					        "node": "*"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
| 
						 | 
					@ -10318,6 +10320,14 @@
 | 
				
			||||||
      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
 | 
					      "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/simple-imap/node_modules/moment": {
 | 
				
			||||||
 | 
					      "version": "2.8.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/simple-imap/node_modules/readable-stream": {
 | 
					    "node_modules/simple-imap/node_modules/readable-stream": {
 | 
				
			||||||
      "version": "1.1.14",
 | 
					      "version": "1.1.14",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@
 | 
				
			||||||
    "dayjs": "^1.11.13",
 | 
					    "dayjs": "^1.11.13",
 | 
				
			||||||
    "imap": "^0.8.19",
 | 
					    "imap": "^0.8.19",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
 | 
					    "moment": "^2.30.1",
 | 
				
			||||||
    "multer": "^1.4.5-lts.1",
 | 
					    "multer": "^1.4.5-lts.1",
 | 
				
			||||||
    "mysql2": "^3.13.0",
 | 
					    "mysql2": "^3.13.0",
 | 
				
			||||||
    "nestjs-paginate": "^11.1.0",
 | 
					    "nestjs-paginate": "^11.1.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,14 +5,27 @@ import { BadRequestExceptionFilter } from './system/filters/bad-request-exceptio
 | 
				
			||||||
import { IoAdapter } from '@nestjs/platform-socket.io';
 | 
					import { IoAdapter } from '@nestjs/platform-socket.io';
 | 
				
			||||||
import * as cookieParser from 'cookie-parser';
 | 
					import * as cookieParser from 'cookie-parser';
 | 
				
			||||||
import { useContainer } from 'class-validator';
 | 
					import { useContainer } from 'class-validator';
 | 
				
			||||||
 | 
					import { WebBidsService } from './modules/bids/services/web-bids.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function bootstrap() {
 | 
					async function bootstrap() {
 | 
				
			||||||
  const app = await NestFactory.create(AppModule);
 | 
					  const app = await NestFactory.create(AppModule);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const prefix_version = '/api/v1';
 | 
					  const prefix_version = '/api/v1';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const webBidService = app.get<WebBidsService>(WebBidsService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const webs = await webBidService.webBidRepo.find();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const urls = webs.map((web) => web.origin_url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const corsOrigins = process.env.CORS
 | 
				
			||||||
 | 
					    ? process.env.CORS.split(',').map((origin) => origin.trim())
 | 
				
			||||||
 | 
					    : ['*']; // hoặc [] tùy ý bạn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('%csrc/main.ts:26 [...corsOrigins, ...urls]', 'color: #007acc;', [...corsOrigins, ...urls]);
 | 
				
			||||||
  app.enableCors({
 | 
					  app.enableCors({
 | 
				
			||||||
    origin: process.env.CORS.split(', ') || '*',
 | 
					    origin: [...corsOrigins, ...urls],
 | 
				
			||||||
    methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
 | 
					    methods: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
 | 
				
			||||||
    allowedHeaders: 'Content-Type, Authorization',
 | 
					    allowedHeaders: 'Content-Type, Authorization',
 | 
				
			||||||
    credentials: true,
 | 
					    credentials: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,4 +103,5 @@ export class GenerateKeysService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(result);
 | 
					    return AppResponse.toResponse(result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,8 @@ import { WebBidsService } from '../../services/web-bids.service';
 | 
				
			||||||
import { Event } from '../../utils/events';
 | 
					import { Event } from '../../utils/events';
 | 
				
			||||||
import AppResponse from '@/response/app-response';
 | 
					import AppResponse from '@/response/app-response';
 | 
				
			||||||
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
 | 
					import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
 | 
				
			||||||
 | 
					import { UpdateBidDto } from '../../dto/bid/update-bid.dto';
 | 
				
			||||||
 | 
					import { ClientInfoUpdateBidDto } from '../../dto/bid/client-info-update-bid.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller('bids')
 | 
					@Controller('bids')
 | 
				
			||||||
export class BidsController {
 | 
					export class BidsController {
 | 
				
			||||||
| 
						 | 
					@ -36,9 +38,14 @@ export class BidsController {
 | 
				
			||||||
    return this.bidsService.clientIndex();
 | 
					    return this.bidsService.clientIndex();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get(':model')
 | 
				
			||||||
 | 
					  getByModel(@Param('model') model: Bid['model']) {
 | 
				
			||||||
 | 
					    return this.bidsService.getBidByModel(model);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post()
 | 
					  @Post()
 | 
				
			||||||
  create(@Body() data: CreateBidDto) {
 | 
					  create(@Body() data: CreateBidDto) {
 | 
				
			||||||
    return this.bidsService.create(data);
 | 
					    return this.bidsService.create(data, (result) => !!result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Put(':id')
 | 
					  @Put(':id')
 | 
				
			||||||
| 
						 | 
					@ -46,6 +53,11 @@ export class BidsController {
 | 
				
			||||||
    return this.bidsService.clientUpdate(id, data);
 | 
					    return this.bidsService.clientUpdate(id, data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Put('info/:id')
 | 
				
			||||||
 | 
					  updateInfo(@Param('id') id: Bid['id'], @Body() data: ClientInfoUpdateBidDto) {
 | 
				
			||||||
 | 
					    return this.bidsService.update(id, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('out-bid/:id')
 | 
					  @Post('out-bid/:id')
 | 
				
			||||||
  outBidStatus(@Param('id') id: Bid['id']) {
 | 
					  outBidStatus(@Param('id') id: Bid['id']) {
 | 
				
			||||||
    return this.bidsService.outBid(id);
 | 
					    return this.bidsService.outBid(id);
 | 
				
			||||||
| 
						 | 
					@ -70,10 +82,8 @@ export class BidsController {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('update-login-status')
 | 
					  @Post('update-login-status')
 | 
				
			||||||
  async updateLoginStatus(
 | 
					  async updateLoginStatus(@Body() data: ClientUpdateLoginStatusDto) {
 | 
				
			||||||
    @Body() data: ClientUpdateLoginStatusDto
 | 
					    return await this.bidsService.emitLoginStatus(data);
 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return await this.bidsService.emitLoginStatus(data)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Post('test')
 | 
					  @Post('test')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import { IsNumber, IsOptional } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class ClientInfoUpdateBidDto {
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  max_price: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  quantity: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsNumber()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  plus_price: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,16 @@
 | 
				
			||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
 | 
					import {
 | 
				
			||||||
 | 
					  Column,
 | 
				
			||||||
 | 
					  Entity,
 | 
				
			||||||
 | 
					  JoinColumn,
 | 
				
			||||||
 | 
					  ManyToOne,
 | 
				
			||||||
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
 | 
					  Unique,
 | 
				
			||||||
 | 
					} from 'typeorm';
 | 
				
			||||||
import { Timestamp } from './timestamp';
 | 
					import { Timestamp } from './timestamp';
 | 
				
			||||||
import { Bid } from './bid.entity';
 | 
					import { Bid } from './bid.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Entity('send_message_histories')
 | 
					@Entity('send_message_histories')
 | 
				
			||||||
@Unique(['max_price', 'type', 'reserve_price'])
 | 
					@Unique(['max_price', 'type', 'reserve_price', 'bid'])
 | 
				
			||||||
export class SendMessageHistory extends Timestamp {
 | 
					export class SendMessageHistory extends Timestamp {
 | 
				
			||||||
  @PrimaryGeneratedColumn('increment')
 | 
					  @PrimaryGeneratedColumn('increment')
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					import { NotificationService } from '@/modules/notification/notification.service';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BadRequestException,
 | 
					  BadRequestException,
 | 
				
			||||||
  HttpStatus,
 | 
					  HttpStatus,
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,7 @@ import AppResponse from 'src/response/app-response';
 | 
				
			||||||
import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
 | 
					import { extractModelId, isTimeReached, subtractMinutes } from 'src/ultils';
 | 
				
			||||||
import { In, IsNull, Not, Repository } from 'typeorm';
 | 
					import { In, IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
					import { ClientUpdateBidDto } from '../dto/bid/client-update-bid.dto';
 | 
				
			||||||
 | 
					import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
 | 
				
			||||||
import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
					import { CreateBidDto } from '../dto/bid/create-bid.dto';
 | 
				
			||||||
import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
					import { UpdateBidDto } from '../dto/bid/update-bid.dto';
 | 
				
			||||||
import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
 | 
					import { UpdateStatusByPriceDto } from '../dto/bid/update-status-by-price.dto';
 | 
				
			||||||
| 
						 | 
					@ -28,11 +30,8 @@ import { BidHistory } from '../entities/bid-history.entity';
 | 
				
			||||||
import { Bid } from '../entities/bid.entity';
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
					import { ImageCompressionPipe } from '../pipes/image-compression-pipe';
 | 
				
			||||||
import { Constant } from '../utils/constant';
 | 
					import { Constant } from '../utils/constant';
 | 
				
			||||||
import { WebBidsService } from './web-bids.service';
 | 
					 | 
				
			||||||
import { NotificationService } from '@/modules/notification/notification.service';
 | 
					 | 
				
			||||||
import { Event } from '../utils/events';
 | 
					import { Event } from '../utils/events';
 | 
				
			||||||
import _ from 'lodash';
 | 
					import { WebBidsService } from './web-bids.service';
 | 
				
			||||||
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class BidsService {
 | 
					export class BidsService {
 | 
				
			||||||
| 
						 | 
					@ -100,7 +99,7 @@ export class BidsService {
 | 
				
			||||||
    await this.webBidsService.emitAllBidEvent();
 | 
					    await this.webBidsService.emitAllBidEvent();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create(data: CreateBidDto) {
 | 
					  async create(data: CreateBidDto, response?: (result: Bid) => any) {
 | 
				
			||||||
    const model = extractModelId(data.url);
 | 
					    const model = extractModelId(data.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
					    const prev = await this.bidsRepo.findOne({ where: { model: model } });
 | 
				
			||||||
| 
						 | 
					@ -121,7 +120,7 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.emitAllBidEvent();
 | 
					    await this.emitAllBidEvent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppResponse.toResponse(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) {
 | 
				
			||||||
| 
						 | 
					@ -226,7 +225,7 @@ export class BidsService {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
 | 
					    // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc
 | 
				
			||||||
    if (!bid.close_time && !bid.start_bid_time) {
 | 
					    if (!bid.close_time && !bid.start_bid_time && data?.lot_id) {
 | 
				
			||||||
      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
					      // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng
 | 
				
			||||||
      // bid.start_bid_time = new Date().toUTCString();
 | 
					      // bid.start_bid_time = new Date().toUTCString();
 | 
				
			||||||
      bid.start_bid_time = subtractMinutes(
 | 
					      bid.start_bid_time = subtractMinutes(
 | 
				
			||||||
| 
						 | 
					@ -549,4 +548,14 @@ export class BidsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return nextBid;
 | 
					    return nextBid;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getBidByModel(model: string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log('%csrc/modules/bids/services/bids.service.ts:554 model', 'color: #007acc;', model);
 | 
				
			||||||
 | 
					    const bid = await this.bidsRepo.findOne({ where: { model } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!bid) return AppResponse.toResponse(null, {status_code: HttpStatus.NOT_FOUND});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppResponse.toResponse(plainToClass(Bid, bid));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,9 +20,9 @@ export class ConfigsService {
 | 
				
			||||||
    return await this.configRepo.findOne({ where: { key_name } }) || null;
 | 
					    return await this.configRepo.findOne({ where: { key_name } }) || null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string) {
 | 
					  async setConfig(key_name: keyof typeof ConfigsService.CONFIG_KEYS, value: string, type: 'string' | 'number') {
 | 
				
			||||||
    return await this.configRepo.upsert(
 | 
					    return await this.configRepo.upsert(
 | 
				
			||||||
      { key_name, value },
 | 
					      { key_name, value , type},
 | 
				
			||||||
      ['key_name'] 
 | 
					      ['key_name'] 
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,9 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
import { Cron, CronExpression } from '@nestjs/schedule';
 | 
					import { Cron, CronExpression } from '@nestjs/schedule';
 | 
				
			||||||
import { IsNull, Not } from 'typeorm';
 | 
					import { IsNull, Not } from 'typeorm';
 | 
				
			||||||
import { BidsService } from './bids.service';
 | 
					import { BidsService } from './bids.service';
 | 
				
			||||||
import { isTimeReached, subtractMinutes, subtractSeconds } from '@/ultils';
 | 
					import * as moment from 'moment';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  isTimeReached,
 | 
				
			||||||
 | 
					  shouldResetTool,
 | 
				
			||||||
 | 
					  subtractMinutes,
 | 
				
			||||||
 | 
					  subtractSeconds,
 | 
				
			||||||
 | 
					} from '@/ultils';
 | 
				
			||||||
import { ConfigsService } from './configs.service';
 | 
					import { ConfigsService } from './configs.service';
 | 
				
			||||||
import { DashboardService } from './dashboard.service';
 | 
					import { DashboardService } from './dashboard.service';
 | 
				
			||||||
 | 
					import { Bid } from '../entities/bid.entity';
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class TasksService {
 | 
					export class TasksService {
 | 
				
			||||||
  private readonly logger = new Logger(TasksService.name);
 | 
					  private readonly logger = new Logger(TasksService.name);
 | 
				
			||||||
| 
						 | 
					@ -17,52 +24,57 @@ export class TasksService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Cron(CronExpression.EVERY_MINUTE)
 | 
					  @Cron(CronExpression.EVERY_MINUTE)
 | 
				
			||||||
  async handleCron() {
 | 
					  async handleCron() {
 | 
				
			||||||
    const nextBid = await this.bidsService.getNextBid();
 | 
					    const bids = await this.bidsService.bidsRepo.find({
 | 
				
			||||||
 | 
					      where: { status: 'biding' },
 | 
				
			||||||
 | 
					      select: ['close_time', 'created_at', 'start_bid_time', 'id', 'lot_id'],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!nextBid) return;
 | 
					    const lastResetTimeRaw =
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const timeReset = subtractMinutes(nextBid.close_time, 20);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const timeToTracking = subtractSeconds(
 | 
					 | 
				
			||||||
      nextBid.close_time,
 | 
					 | 
				
			||||||
      nextBid.web_bid.early_tracking_seconds,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!isTimeReached(timeReset) || isTimeReached(timeToTracking)) {
 | 
					 | 
				
			||||||
      console.log('Reset not allowed at this time');
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const lastestResetToolTime =
 | 
					 | 
				
			||||||
      await this.configsService.getConfig('REFRESH_TOOL_TIME');
 | 
					      await this.configsService.getConfig('REFRESH_TOOL_TIME');
 | 
				
			||||||
 | 
					    const lastResetTime = lastResetTimeRaw?.value
 | 
				
			||||||
 | 
					      ? new Date(lastResetTimeRaw.value)
 | 
				
			||||||
 | 
					      : null;
 | 
				
			||||||
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lastestResetToolTime?.value) {
 | 
					    console.log('===== Reset Tool Check =====');
 | 
				
			||||||
      const lastReset = Date.parse(lastestResetToolTime.value);
 | 
					    console.log(
 | 
				
			||||||
      const now = Date.now();
 | 
					      'Last reset time:',
 | 
				
			||||||
 | 
					      lastResetTime
 | 
				
			||||||
 | 
					        ? moment(lastResetTime).format('YYYY-MM-DD HH:mm:ss')
 | 
				
			||||||
 | 
					        : 'null',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    console.log('Current time:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const diffInMs = now - lastReset;
 | 
					    const result = shouldResetTool(bids, lastResetTime, now);
 | 
				
			||||||
      const diffInHours = diffInMs / (1000 * 60 * 60);
 | 
					 | 
				
			||||||
      const minimumHours = 2;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (diffInHours < minimumHours) {
 | 
					    if (result.shouldReset) {
 | 
				
			||||||
        console.log(`Last reset was less than ${minimumHours} hours ago`);
 | 
					      console.log('Should reset due to:', result.reason);
 | 
				
			||||||
        return;
 | 
					      if (result.bidId) {
 | 
				
			||||||
 | 
					        console.log('Related bid id:', result.bidId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (result.closeTime) {
 | 
				
			||||||
 | 
					          const closeTimeDate = new Date(result.closeTime);
 | 
				
			||||||
 | 
					          const expectedReset = moment(closeTimeDate)
 | 
				
			||||||
 | 
					            .subtract(20, 'minutes')
 | 
				
			||||||
 | 
					            .format('YYYY-MM-DD HH:mm:ss');
 | 
				
			||||||
 | 
					          console.log(
 | 
				
			||||||
 | 
					            'Expected reset time (close_time - 20 minutes):',
 | 
				
			||||||
 | 
					            expectedReset,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log('Proceeding to reset tool for next bid:', nextBid);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await this.dashboadService.resetTool();
 | 
					      await this.dashboadService.resetTool();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const time = new Date().toUTCString()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await this.configsService.setConfig(
 | 
					      await this.configsService.setConfig(
 | 
				
			||||||
        'REFRESH_TOOL_TIME',
 | 
					        'REFRESH_TOOL_TIME',
 | 
				
			||||||
      time,
 | 
					        now.toISOString(),
 | 
				
			||||||
 | 
					        'string',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log('Reset successfully at: ' + time);
 | 
					      console.log('Tool reset at:', moment(now).format('YYYY-MM-DD HH:mm:ss'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log('No reset needed at this time');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,5 @@
 | 
				
			||||||
 | 
					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)) {
 | 
				
			||||||
    case 'https://www.grays.com': {
 | 
					    case 'https://www.grays.com': {
 | 
				
			||||||
| 
						 | 
					@ -107,3 +109,72 @@ export function verifyCode(content: string) {
 | 
				
			||||||
  // Nếu không tìm thấy mã xác minh, trả về null
 | 
					  // Nếu không tìm thấy mã xác minh, trả về null
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function shouldResetTool(
 | 
				
			||||||
 | 
					    bids: Bid[],
 | 
				
			||||||
 | 
					    lastResetTime: Date | null,
 | 
				
			||||||
 | 
					    now: Date = new Date(),
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const ONE_MINUTE = 60 * 1000;
 | 
				
			||||||
 | 
					    const ONE_HOUR = 60 * ONE_MINUTE;
 | 
				
			||||||
 | 
					    const TWO_HOURS = 2 * ONE_HOUR;
 | 
				
			||||||
 | 
					    const TWENTY_MINUTES = 20 * 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
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      shouldReset: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -84,6 +84,9 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
        configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
 | 
					        configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
 | 
				
			||||||
        this.id
 | 
					        this.id
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // extractLotsAndBids
 | 
				
			||||||
 | 
					      await this.extractLotsAndBids();
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      if (error.message.includes("Execution context was destroyed")) {
 | 
					      if (error.message.includes("Execution context was destroyed")) {
 | 
				
			||||||
        console.log(
 | 
					        console.log(
 | 
				
			||||||
| 
						 | 
					@ -172,6 +175,32 @@ export class GrayApiBid extends ApiBid {
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async extractLotsAndBids(url) {
 | 
				
			||||||
 | 
					    if (!this.page_context) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await this.page_context.evaluate(() => {
 | 
				
			||||||
 | 
					      const rows = document.querySelectorAll("tr.current.open");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return Array.from(rows).map((row) => {
 | 
				
			||||||
 | 
					        const lotText =
 | 
				
			||||||
 | 
					          row.querySelector(".lot-number")?.textContent.trim() || "";
 | 
				
			||||||
 | 
					        const autobidMatch = row.textContent.match(
 | 
				
			||||||
 | 
					          /Your maximum autobid is AU\s*\$([\d,]+)/
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        const lotMatch = lotText.match(/Lot No:\s*(\S+)/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          model: lotMatch ? lotMatch[1] : null,
 | 
				
			||||||
 | 
					          max_price: autobidMatch
 | 
				
			||||||
 | 
					            ? parseFloat(autobidMatch[1].replace(/,/g, ""))
 | 
				
			||||||
 | 
					            : null,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    global[`BIDED_DATA_${this.origin_url}`] = data || [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleLogin() {
 | 
					  async handleLogin() {
 | 
				
			||||||
    const page = this.page_context;
 | 
					    const page = this.page_context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,13 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  outBid,
 | 
					 | 
				
			||||||
  pushPrice,
 | 
					  pushPrice,
 | 
				
			||||||
  updateBid,
 | 
					  updateBid,
 | 
				
			||||||
  updateStatusByPrice,
 | 
					  updateStatusByPrice,
 | 
				
			||||||
} from "../../system/apis/bid.js";
 | 
					} from "../../system/apis/bid.js";
 | 
				
			||||||
import CONSTANTS from "../../system/constants.js";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  delay,
 | 
					  delay,
 | 
				
			||||||
  extractNumber,
 | 
					  extractNumber,
 | 
				
			||||||
  isNumber,
 | 
					 | 
				
			||||||
  isTimeReached,
 | 
					  isTimeReached,
 | 
				
			||||||
  removeFalsyValues,
 | 
					  removeFalsyValues,
 | 
				
			||||||
  safeClosePage,
 | 
					 | 
				
			||||||
  takeSnapshot,
 | 
					 | 
				
			||||||
} from "../../system/utils.js";
 | 
					} from "../../system/utils.js";
 | 
				
			||||||
import { ProductBid } from "../product-bid.js";
 | 
					import { ProductBid } from "../product-bid.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +60,12 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
  async isCloseProduct() {
 | 
					  async isCloseProduct() {
 | 
				
			||||||
    const close_time = await this.getCloseTime();
 | 
					    const close_time = await this.getCloseTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentUrl = await this.page_context.url();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (currentUrl !== this.url) {
 | 
				
			||||||
 | 
					      return { result: false, close_time };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!close_time) {
 | 
					    if (!close_time) {
 | 
				
			||||||
      const priceWasBid = await this.getPriceWasBid();
 | 
					      const priceWasBid = await this.getPriceWasBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,6 +131,7 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      this.page_context.goto(this.url);
 | 
				
			||||||
      console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
 | 
					      console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`);
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -301,7 +303,6 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const page = this.page_context;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (global[`IS_PLACE_BID-${this.id}`]) {
 | 
					    if (global[`IS_PLACE_BID-${this.id}`]) {
 | 
				
			||||||
      console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
 | 
					      console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
 | 
				
			||||||
| 
						 | 
					@ -316,17 +317,50 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
      global.IS_CLEANING = false;
 | 
					      global.IS_CLEANING = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const isCloseProduct = await this.isCloseProduct();
 | 
					      const isCloseProduct = await this.isCloseProduct();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isCloseProduct.result) {
 | 
					      if (isCloseProduct.result) {
 | 
				
			||||||
        console.log(
 | 
					        console.log(
 | 
				
			||||||
          `⚠️ [${this.id}] Outbid detected, calling outBid function.`
 | 
					          `⚠️ [${this.id}] Outbid detected, calling outBid function.`
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await outBid(this.id);
 | 
					        await outBid(this.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const biddedData = this.getBidedData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log({
 | 
				
			||||||
 | 
					        biddedData,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isBided = biddedData.find(
 | 
				
			||||||
 | 
					        (item) => item.model === this.model && item.max_price === this.max_price
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isBided) {
 | 
				
			||||||
 | 
					        console.log(`[${this.id}] This item bided. Skipping...`);
 | 
				
			||||||
 | 
					        global[`IS_PLACE_BID-${this.id}`] = false;
 | 
				
			||||||
 | 
					        global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!this.start_bid_time || !this.close_time) {
 | 
				
			||||||
 | 
					        console.log(
 | 
				
			||||||
 | 
					          `[${this.id}] Skipping processing: auction has started but not yet closed.`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return;
 | 
					        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
 | 
					      // 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) {
 | 
					      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
 | 
					        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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return; // Dừng hàm nếu giá đã vượt qua giới hạn
 | 
					        return; // Dừng hàm nếu giá đã vượt qua giới hạn
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -337,6 +371,8 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
            this.name || "None"
 | 
					            this.name || "None"
 | 
				
			||||||
          }`
 | 
					          }`
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					        global.IS_CLEANING = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -347,6 +383,9 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      console.log(
 | 
				
			||||||
 | 
					        "-------------------------------------BIDING---------------------------------------"
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      const result = await this.placeBid();
 | 
					      const result = await this.placeBid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      global.IS_CLEANING = true;
 | 
					      global.IS_CLEANING = true;
 | 
				
			||||||
| 
						 | 
					@ -358,6 +397,10 @@ export class GraysProductBid extends ProductBid {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBidedData() {
 | 
				
			||||||
 | 
					    return global[`BIDED_DATA_${this.web_bid?.origin_url}`];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  action = async () => {
 | 
					  action = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const page = this.page_context;
 | 
					      const page = this.page_context;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -0,0 +1,134 @@
 | 
				
			||||||
 | 
					/* #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.
										
									
								
							| 
		 After Width: | Height: | Size: 4.0 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 305 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 522 B  | 
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					chrome.action.onClicked.addListener((tab) => {
 | 
				
			||||||
 | 
					  // Lấy URL của tab hiện tại
 | 
				
			||||||
 | 
					  console.log("Current URL:", tab.url);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					// config.js
 | 
				
			||||||
 | 
					const CONFIG = {
 | 
				
			||||||
 | 
					  API_BASE_URL: "http://localhost:4000/api/v1",
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default CONFIG;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,394 @@
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,170 @@
 | 
				
			||||||
 | 
					<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>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					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