diff --git a/src/database/migrations/1730859187883-add-queue-bucket.ts b/src/database/migrations/1730859187883-add-queue-bucket.ts new file mode 100644 index 0000000..9e06f8d --- /dev/null +++ b/src/database/migrations/1730859187883-add-queue-bucket.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddQueueBucket1730859187883 implements MigrationInterface { + name = 'AddQueueBucket1730859187883'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "queue_bucket" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "queue_item_id" character varying NOT NULL, "date" bigint NOT NULL, "regular" integer NOT NULL, "vip" integer NOT NULL, CONSTRAINT "PK_cdd58b0d9e93e4be922da9d8bd6" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "queue_bucket"`); + } +} diff --git a/src/modules/queue/data/services/queue-bucket.ts b/src/modules/queue/data/services/queue-bucket.ts index 9186137..b4c69de 100644 --- a/src/modules/queue/data/services/queue-bucket.ts +++ b/src/modules/queue/data/services/queue-bucket.ts @@ -2,9 +2,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; -import { Repository } from 'typeorm'; +import { Between, Repository } from 'typeorm'; import { BaseReadService } from 'src/core/modules/data/service/base-read.service'; import { QueueBucketModel } from '../models/queue-bucket.model'; +import * as moment from 'moment'; @Injectable() export class QueueBucketReadService extends BaseReadService { @@ -15,11 +16,43 @@ export class QueueBucketReadService extends BaseReadService { super(repo); } - getQueue(item_id: string): Promise { - return this.repo.findOne({ + async getQueue(item_id: string, vip = false): Promise { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + const queue = await this.repo.findOne({ where: { queue_item_id: item_id, + date: Between(start, end), }, }); + + if (!queue) { + const regularNumber = vip ? 0 : 1; + const vipNumber = vip ? 1 : 0; + this.repo.save({ + queue_item_id: item_id, + date: start, + regular: regularNumber, + vip: vipNumber, + }); + return Promise.resolve(1); + } else { + const field = vip ? 'vip' : 'regular'; + const data = await this.repo + .createQueryBuilder('bucket') + .update(QueueBucketModel) + .set({ + [field]: () => `${field} + 1`, + }) + .where('id = :key', { + key: queue.id, + }) + .returning(field) + .updateEntity(true) + .execute(); + + return data.raw[0]?.[field] ?? 1; + } } } diff --git a/src/modules/queue/data/services/queue.service.ts b/src/modules/queue/data/services/queue.service.ts index 786820c..b015301 100644 --- a/src/modules/queue/data/services/queue.service.ts +++ b/src/modules/queue/data/services/queue.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; import { QueueItemModel, QueueModel, @@ -19,6 +19,18 @@ export class QueueDataService extends BaseReadService { ) { super(repo); } + + async queueItems(item_queue_id: string[]): Promise { + return this.repo.find({ + relations: ['item', 'item.item', 'item.item.item_queue'], + where: { + item: { item: { item_queue: { id: In(item_queue_id) } } }, + }, + order: { + time: 'DESC', + }, + }); + } } @Injectable() diff --git a/src/modules/queue/domain/helpers/time.helper.ts b/src/modules/queue/domain/helpers/time.helper.ts new file mode 100644 index 0000000..88f094e --- /dev/null +++ b/src/modules/queue/domain/helpers/time.helper.ts @@ -0,0 +1,11 @@ +import * as moment from 'moment'; + +export function toTime(timestamp: number): string { + const date = moment.unix(timestamp / 1000).add(7, 'hours'); + + const hours = date.hours(); + const minutes = date.minutes(); + return `${hours < 10 ? '0' : ''}${hours}:${ + minutes < 10 ? '0' : '' + }${minutes}`; +} diff --git a/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts b/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts new file mode 100644 index 0000000..358f548 --- /dev/null +++ b/src/modules/queue/domain/usecases/formula/queue-condition.formula.ts @@ -0,0 +1,38 @@ +import { QueueModel } from 'src/modules/queue/data/models/queue.model'; +import { toTime } from '../../helpers/time.helper'; +import * as math from 'mathjs'; + +export class QueueCondition { + private ticketItems = {}; + constructor(readonly items: QueueModel[]) { + items.forEach((item) => { + const item_id = item.item.item.item_queue?.id ?? item.item.item.id; + const currentItem = this.ticketItems[item_id]; + this.ticketItems[item_id] = currentItem ? [...currentItem, item] : [item]; + }); + } + + condition(item_id: string) { + const queues: QueueModel[] = this.ticketItems[item_id] ?? []; + const time = queues[0]?.time; + const nearest = time ? toTime(time) : 0; + return { + available: queues.length == 0, + average: this.averageTime(queues), + nearest: nearest, + crowded_level: queues.length, + available_time: nearest, + }; + } + + averageTime(queues: QueueModel[]) { + if (queues.length == 0) return 0; + const calledQueue = queues.filter((q) => q.status === 'called'); + + const times = calledQueue.map((queue) => { + return queue.call_time - queue.time; + }); + + return math.sum(times) / times.length; + } +} diff --git a/src/modules/queue/domain/usecases/index-queue.manager.ts b/src/modules/queue/domain/usecases/index-queue.manager.ts index f94d39f..a379d99 100644 --- a/src/modules/queue/domain/usecases/index-queue.manager.ts +++ b/src/modules/queue/domain/usecases/index-queue.manager.ts @@ -6,6 +6,7 @@ import { RelationParam, } from 'src/core/modules/domain/entities/base-filter.entity'; import { Queue } from '../entities/queue.entity'; +import * as moment from 'moment'; @Injectable() export class IndexQueueManager extends BaseIndexManager { @@ -59,6 +60,14 @@ export class IndexQueueManager extends BaseIndexManager { setQueryFilter( queryBuilder: SelectQueryBuilder, ): SelectQueryBuilder { + const start = moment().startOf('day').valueOf(); + const end = moment().endOf('day').valueOf(); + + queryBuilder.andWhere(`${this.tableName}.time BETWEEN :start AND :end`, { + start, + end, + }); + if (this.filterParam.vip != null) { queryBuilder.andWhere(`${this.tableName}.vip = :vip`, { vip: this.filterParam.vip, diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts index 0ce404d..c54975e 100644 --- a/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts +++ b/src/modules/queue/domain/usecases/queue/customer-queue-detail.manager.ts @@ -1,7 +1,14 @@ +import { QueueModel } from 'src/modules/queue/data/models/queue.model'; import { CustomerQueueManager } from './customer-queue.manager'; +import { QueueCondition } from '../formula/queue-condition.formula'; export class CustomerQueueDetailManager extends CustomerQueueManager { + private queues: QueueModel[] = []; + currentQueues(queues: QueueModel[]) { + this.queues = queues; + } get data() { + const queueCondition = new QueueCondition(this.queues); return this.tickets.map((ticket) => { return { id: ticket.id, @@ -16,10 +23,11 @@ export class CustomerQueueDetailManager extends CustomerQueueManager { total_queue: this.totalQueueTickets(ticket), }, items: ticket.items.map((item) => { + const queueItem = item.item.item_queue ?? item.item; return { id: item.item_id, item_queue_id: item.id, - title: item.item.item_queue?.name ?? item.item.name, + title: queueItem.name, image_url: item.item.image_url, summary: { total_tickets: item.qty, @@ -36,13 +44,7 @@ export class CustomerQueueDetailManager extends CustomerQueueManager { status: q.status, }; }), - queue_condition: { - available: true, - average: 12, - nearest: '13:10', - crowded_level: 20, - available_time: '15:00', - }, + queue_condition: queueCondition.condition(queueItem.id), }; }), }; diff --git a/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts index d43a070..e12625d 100644 --- a/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts +++ b/src/modules/queue/domain/usecases/queue/customer-queue-item-list.manager.ts @@ -1,11 +1,21 @@ -import { QueueItemModel } from 'src/modules/queue/data/models/queue.model'; +import { + QueueItemModel, + QueueModel, +} from 'src/modules/queue/data/models/queue.model'; import { CustomerQueueManager } from './customer-queue.manager'; +import { QueueCondition } from '../formula/queue-condition.formula'; export class CustomerQueueItemListManager extends CustomerQueueManager { + private queues: QueueModel[] = []; + currentQueues(queues: QueueModel[]) { + this.queues = queues; + } get data() { const tickets = this.tickets; const ticketItems = {}; + const queueCondition = new QueueCondition(this.queues); + tickets.forEach((ticket) => { ticket.items.forEach((item) => { const item_id = item.item.item_queue?.id ?? item.item.id; @@ -17,26 +27,16 @@ export class CustomerQueueItemListManager extends CustomerQueueManager { return Object.values(ticketItems).map((items) => { const item = items[0]; const item_qty = items.reduce((acc, item) => acc + item.qty, 0); + const queueItem = item.item.item_queue ?? item.item; return { id: item.item_id, queue_item_id: item.id, - title: item.item.item_queue?.name ?? item.item.name, + title: queueItem.name, image_url: item.item.image_url, qty: item_qty, - available: true, - average: 12, - nearest: '13:10', - crowded_level: 20, - available_time: '15:00', - - queue_condition: { - available: true, - average: 12, - nearest: '13:10', - crowded_level: 20, - available_time: '15:00', - }, + queue_condition: queueCondition.condition(queueItem.id), + ...queueCondition.condition(queueItem.id), }; }); } diff --git a/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts b/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts index 5b95ba1..2faaa8a 100644 --- a/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts +++ b/src/modules/queue/domain/usecases/queue/customer-queue.manager.ts @@ -2,6 +2,7 @@ import { QueueItemModel, QueueTicketModel, } from '../../../data/models/queue.model'; +import { toTime } from '../../helpers/time.helper'; export class CustomerQueueManager { constructor(protected readonly tickets: QueueTicketModel[]) {} @@ -10,13 +11,7 @@ export class CustomerQueueManager { } toTime(timestamp: number): string { - // js function to convert timestamp (1729739455000) to time with format HH:mm - const date = new Date(timestamp / 1000); - const hours = date.getHours() + 7; - const minutes = date.getMinutes(); - return `${hours < 10 ? '0' : ''}${hours}:${ - minutes < 10 ? '0' : '' - }${minutes}`; + return toTime(timestamp); } totalActivities(ticket: QueueTicketModel): number { diff --git a/src/modules/queue/domain/usecases/register-queue.manager.ts b/src/modules/queue/domain/usecases/register-queue.manager.ts index 6d80b11..6ecd67f 100644 --- a/src/modules/queue/domain/usecases/register-queue.manager.ts +++ b/src/modules/queue/domain/usecases/register-queue.manager.ts @@ -7,12 +7,12 @@ import { import { BaseCreateManager } from 'src/core/modules/domain/usecase/managers/base-create.manager'; import { STATUS } from 'src/core/strings/constants/base.constants'; import { QueueModel } from '../../data/models/queue.model'; -import { generateRandom } from 'src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper'; +import { padCode } from 'src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper'; import { QueueBucketReadService } from '../../data/services/queue-bucket'; @Injectable() export class RegisterQueueManager extends BaseCreateManager { - constructor(private readonly queueService: QueueBucketReadService) { + constructor(private readonly bucketService: QueueBucketReadService) { super(); } @@ -22,11 +22,13 @@ export class RegisterQueueManager extends BaseCreateManager { } async beforeProcess(): Promise { + const queueNumber = await this.bucketService.getQueue(this.data.item_id); + const code = `A${padCode(queueNumber)}`; Object.assign(this.data, { status: STATUS.WAITING, time: new Date().getTime(), vip: false, - code: `A${generateRandom(4, true)}`, + code, }); return; } diff --git a/src/modules/queue/infrastructure/handlers/transaction.handler.ts b/src/modules/queue/infrastructure/handlers/transaction.handler.ts index 49d69c3..10c2105 100644 --- a/src/modules/queue/infrastructure/handlers/transaction.handler.ts +++ b/src/modules/queue/infrastructure/handlers/transaction.handler.ts @@ -8,6 +8,7 @@ import { TicketDataService } from '../../data/services/ticket.service'; import { QueueOrder } from '../../domain/entities/order.entity'; import { QueueTicket } from '../../domain/entities/ticket.entity'; import { QueueItem } from '../../domain/entities/queue-item.entity'; +import * as moment from 'moment'; @EventsHandler(TransactionChangeStatusEvent, TransactionCreateQueueEvent) export class QueueTransactionHandler @@ -33,13 +34,16 @@ export class QueueTransactionHandler relations: ['items'], }); + const date = transaction.booking_date ?? transaction.invoice_date; + const queue_date = moment(date, 'YYYY-MM-DD').unix(); + const { id, customer_name, customer_phone, invoice_code } = transaction; - const current_date = new Date().valueOf(); + const customerOrder = { code: invoice_code, customer: customer_name, phone: customer_phone, - date: current_date, + date: queue_date, transaction_id: id, }; diff --git a/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts b/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts index 4c65f72..8295e13 100644 --- a/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts +++ b/src/modules/transaction/vip-code/domain/usecases/managers/helpers/generate-random.helper.ts @@ -22,3 +22,7 @@ export function generateCodeDate() { return `${month}${year}`; } + +export function padCode(number: number, pad = 4): string { + return String(number).padStart(pad, '0'); +}