From 6f8ed0efbbc8044d0cc3ab786a4de2f1dc3567c7 Mon Sep 17 00:00:00 2001 From: Narul Hidayah Date: Sun, 24 May 2026 15:59:31 +0900 Subject: [PATCH] refactor(room_services): enhance service handling and response formatting; update service type handling in service controller --- src/controllers/room_services.ts | 99 +++++++------- src/controllers/service.ts | 28 +++- src/model/room_services.ts | 219 ++++++++++++++++++++++++++++++- src/swagger/builder.js | 2 +- 4 files changed, 288 insertions(+), 60 deletions(-) diff --git a/src/controllers/room_services.ts b/src/controllers/room_services.ts index be81443..dd2b835 100644 --- a/src/controllers/room_services.ts +++ b/src/controllers/room_services.ts @@ -1,4 +1,4 @@ -import { Service, Room, Paging } from "entity"; +import { Paging } from "entity"; import { NextFunction, Response } from "express"; import { Request } from "express-jwt"; import Joi from "joi"; @@ -10,7 +10,6 @@ import { ServiceModel } from "../model/service"; import { RoomServicesModel } from "../model/room_services"; import { RoomModel } from "../model/room"; import { OrmHelper } from "../helpers/orm"; -import moment from "moment"; const log: Logger = new Logger({ name: "[RoomServicesController]", @@ -71,26 +70,38 @@ export class RoomServicesController { .take(param.limit) .getMany(); - // Transform data: Format room with services - const list_data = rooms.map(room => ({ - id: room.id, - room: room.room, - code: room.code, - description: room.description, - status: room.status, - department: room.department ? { - id: room.department.id, - name: room.department.name - } : null, - services: room.services ? room.services.map(service => ({ - id: service.id, - name: service.name, - service_type: service.serviceType ? { - id: service.serviceType.id, - name: service.serviceType.name - } : null, - })) : [] - })); + const { byTypeId, typeMeta } = await RoomServicesModel.getActiveServicesGroupedByType(); + + const list_data = rooms.map((room) => { + const service_types = RoomServicesModel.deriveTypesForRoomWithMap(room, byTypeId, typeMeta); + + return { + id: room.id, + room: room.room, + code: room.code, + description: room.description, + status: room.status, + department: room.department + ? { + id: room.department.id, + name: room.department.name, + } + : null, + service_types, + services: room.services + ? room.services.map((service) => ({ + id: service.id, + name: service.name, + service_type: service.serviceType + ? { + id: service.serviceType.id, + name: service.serviceType.name, + } + : null, + })) + : [], + }; + }); const count_data = list_data.length; @@ -216,26 +227,12 @@ export class RoomServicesController { let room = await RoomModel.list({ "Room.id": param.id }) .then((q) => q.leftJoinAndSelect("Room.services", "s") + .leftJoinAndSelect("s.serviceType", "serviceType") .leftJoinAndSelect("Room.department", "department").getOne()); if (!room) throw { message: "Room " + Language.lang.failed_not_found }; - // Transform data: Format room with services - const detail_data = { - id: room.id, - room: room.room, - code: room.code, - description: room.description, - status: room.status, - department: room.department ? { - id: room.department.id, - name: room.department.name - } : null, - services: room.services ? room.services.map(service => ({ - id: service.id, - name: service.name - })) : [] - }; + const detail_data = await RoomServicesModel.formatRoomResponse(room); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_view, detail_data); } catch (e: unknown) { @@ -266,7 +263,7 @@ export class RoomServicesController { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("Room ID"), - service_ids: Joi.array().items(Joi.string().uuid()).optional().label("Service IDs"), + service_type_ids: Joi.array().items(Joi.string().uuid()).default([]).label("Service Type IDs"), }); let param: any = await schema.validateAsync(req.body); @@ -277,21 +274,21 @@ export class RoomServicesController { if (!room) throw { message: "Room " + Language.lang.failed_not_found }; - let services: any[] = []; - if (param.service_ids && param.service_ids.length > 0) { - for (const serviceId of param.service_ids) { - const service = await ServiceModel.list({ id: serviceId }).then((q) => q.getOne()); - if (service) services.push(service); - } - } - - room.services = services; - room.updated_by = req.auth.data.name; - await queryRunner.manager.save(room); + room = await RoomServicesModel.applyTypesToRoom( + queryRunner.manager, + room, + param.service_type_ids ?? [], + req.auth.data.name + ); await queryRunner.commitTransaction(); - return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, room); + const response_data = await RoomServicesModel.formatRoomResponse( + room, + queryRunner.manager + ); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, response_data); } catch (e: unknown) { await queryRunner.rollbackTransaction(); log.error(e); diff --git a/src/controllers/service.ts b/src/controllers/service.ts index 78cc768..5dae172 100644 --- a/src/controllers/service.ts +++ b/src/controllers/service.ts @@ -1,4 +1,4 @@ -import { Service, Paging } from "entity"; +import { Service, Paging, Status } from "entity"; import { NextFunction, Response } from "express"; import { Request } from "express-jwt"; import Joi from "joi"; @@ -9,7 +9,7 @@ import { Language } from "../langs/lang"; import { ServiceModel } from "../model/service"; import { ServiceTypeModel } from "../model/service_type"; import { ServiceFareModel } from "../model/service_fare"; -import { RoomModel } from "../model/room"; +import { RoomServicesModel } from "../model/room_services"; import { OrmHelper } from "../helpers/orm"; import moment from "moment"; @@ -104,6 +104,12 @@ export class ServiceController { await queryRunner.commitTransaction(); + const saved = await ServiceModel.list({ "Service.id": service.id }) + .then((q) => q.leftJoinAndSelect("Service.serviceType", "serviceType").getOne()); + if (saved) { + await RoomServicesModel.syncRoomsOnServiceCreated(OrmHelper.DB.manager, saved); + } + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, service); } catch (e: unknown) { await queryRunner.rollbackTransaction(); @@ -178,6 +184,8 @@ export class ServiceController { if (!service) throw { message: "Service " + Language.lang.failed_not_found }; + const oldTypeId = service.serviceType?.id; + service.name = param.name; service.serviceType = serviceType; service.status = param.status; @@ -186,6 +194,16 @@ export class ServiceController { await queryRunner.commitTransaction(); + const saved = await ServiceModel.list({ "Service.id": service.id }) + .then((q) => q.leftJoinAndSelect("Service.serviceType", "serviceType").getOne()); + if (saved) { + await RoomServicesModel.syncRoomsOnServiceUpdated( + OrmHelper.DB.manager, + saved, + oldTypeId + ); + } + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_update, service); } catch (e: unknown) { await queryRunner.rollbackTransaction(); @@ -278,6 +296,7 @@ export class ServiceController { } if (affected > 0) { + await RoomServicesModel.syncRoomsOnServiceDeleted(OrmHelper.DB.manager, param.id); return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); @@ -307,6 +326,11 @@ export class ServiceController { const affected = (await repo.restore({ id: param.id })).affected ?? 0; if (affected > 0) { + const service = await ServiceModel.list({ "Service.id": param.id }) + .then((q) => q.leftJoinAndSelect("Service.serviceType", "serviceType").getOne()); + if (service && service.status === Status.Active) { + await RoomServicesModel.syncRoomsOnServiceCreated(OrmHelper.DB.manager, service); + } return ReturnHelper.successResponseAny(res, 200, Language.lang.success_restore, {}); } else { return ReturnHelper.errorResponse(res, 404, 401, Language.lang.failed_not_found, ""); diff --git a/src/model/room_services.ts b/src/model/room_services.ts index bc350e3..51b6668 100644 --- a/src/model/room_services.ts +++ b/src/model/room_services.ts @@ -1,8 +1,9 @@ // RoomServicesModel.ts -import { Service, Room } from "entity"; -import { SelectQueryBuilder } from "typeorm"; +import { Room, Service, Status } from "entity"; +import { EntityManager, In, SelectQueryBuilder } from "typeorm"; import CommonHelper from "../helpers/common"; import { OrmHelper } from "../helpers/orm"; +import { ServiceModel } from "./service"; export class RoomServicesModel { static async listByService(filter = {}, withDeleted = false): Promise> { @@ -59,11 +60,9 @@ export class RoomServicesModel { return query; } - // Method untuk count rooms (bukan relationships) static async countRooms(filter = {}, withDeleted = false): Promise { const repo = OrmHelper.DB.getRepository(Service); - // Query untuk menghitung total unique rooms yang punya services let queryBuilder = repo.createQueryBuilder("Service") .innerJoin("Service.rooms", "Room") .select("COUNT(DISTINCT Room.id)", "count"); @@ -83,11 +82,9 @@ export class RoomServicesModel { return parseInt(result.count) || 0; } - // Method untuk count services static async countServices(filter = {}, withDeleted = false): Promise { const repo = OrmHelper.DB.getRepository(Room); - // Query untuk menghitung total unique services yang punya rooms let queryBuilder = repo.createQueryBuilder("Room") .innerJoin("Room.services", "Service") .select("COUNT(DISTINCT Service.id)", "count"); @@ -106,4 +103,214 @@ export class RoomServicesModel { const result = await queryBuilder.getRawOne(); return parseInt(result.count) || 0; } + + static async expandTypeIdsToServices( + manager: EntityManager, + serviceTypeIds: string[] + ): Promise { + if (!serviceTypeIds || serviceTypeIds.length === 0) { + return []; + } + + return manager.getRepository(Service).find({ + where: { + serviceType: { id: In(serviceTypeIds) }, + status: Status.Active, + }, + relations: ["serviceType"], + }); + } + + static async getActiveServicesGroupedByType(manager: EntityManager = OrmHelper.DB.manager) { + const services = await manager.getRepository(Service).find({ + where: { status: Status.Active }, + relations: ["serviceType"], + }); + + const byTypeId = new Map(); + const typeMeta = new Map(); + + for (const service of services) { + const typeId = service.serviceType?.id; + if (!typeId) { + continue; + } + + if (!byTypeId.has(typeId)) { + byTypeId.set(typeId, []); + } + byTypeId.get(typeId)!.push(service.id); + + if (!typeMeta.has(typeId)) { + typeMeta.set(typeId, { id: typeId, name: service.serviceType!.name }); + } + } + + return { byTypeId, typeMeta }; + } + + static deriveTypesForRoomWithMap( + room: Room, + byTypeId: Map, + typeMeta: Map + ): { id: string; name: string }[] { + const roomServiceIds = new Set((room.services || []).map((s) => s.id)); + const derived: { id: string; name: string }[] = []; + + for (const [typeId, serviceIds] of byTypeId) { + if (serviceIds.length === 0) { + continue; + } + + const allInRoom = serviceIds.every((id) => roomServiceIds.has(id)); + if (allInRoom) { + const meta = typeMeta.get(typeId); + if (meta) { + derived.push(meta); + } + } + } + + return derived.sort((a, b) => a.name.localeCompare(b.name)); + } + + static async deriveTypesForRoom( + room: Room, + manager: EntityManager = OrmHelper.DB.manager + ): Promise<{ id: string; name: string }[]> { + const { byTypeId, typeMeta } = await RoomServicesModel.getActiveServicesGroupedByType(manager); + return RoomServicesModel.deriveTypesForRoomWithMap(room, byTypeId, typeMeta); + } + + static async formatRoomResponse( + room: Room, + manager: EntityManager = OrmHelper.DB.manager + ) { + const service_types = await RoomServicesModel.deriveTypesForRoom(room, manager); + + return { + id: room.id, + room: room.room, + code: room.code, + description: room.description, + status: room.status, + department: room.department + ? { + id: room.department.id, + name: room.department.name, + } + : null, + service_types, + services: room.services + ? room.services.map((service) => ({ + id: service.id, + name: service.name, + service_type: service.serviceType + ? { + id: service.serviceType.id, + name: service.serviceType.name, + } + : null, + })) + : [], + }; + } + + static async applyTypesToRoom( + manager: EntityManager, + room: Room, + serviceTypeIds: string[], + updatedBy: string + ): Promise { + const services = await RoomServicesModel.expandTypeIdsToServices(manager, serviceTypeIds); + room.services = services; + room.updated_by = updatedBy; + return manager.save(room); + } + + static async removeServiceFromAllRooms(manager: EntityManager, serviceId: string): Promise { + await manager + .createQueryBuilder() + .delete() + .from("service_rooms") + .where("service_id = :serviceId", { serviceId }) + .execute(); + } + + static async syncRoomsOnServiceCreated( + manager: EntityManager, + service: Service + ): Promise { + const typeId = service.serviceType?.id; + if (!typeId) { + return; + } + + const othersOfType = await manager.getRepository(Service).find({ + where: { + serviceType: { id: typeId }, + status: Status.Active, + }, + }); + + const otherIds = othersOfType.filter((s) => s.id !== service.id).map((s) => s.id); + if (otherIds.length === 0) { + return; + } + + const rooms = await manager.getRepository(Room).find({ + relations: ["services"], + }); + + for (const room of rooms) { + const roomIds = new Set((room.services || []).map((s) => s.id)); + const hadFullType = otherIds.every((id) => roomIds.has(id)); + if (!hadFullType) { + continue; + } + if (roomIds.has(service.id)) { + continue; + } + room.services = [...(room.services || []), service]; + await manager.save(room); + } + } + + static async syncRoomsOnServiceUpdated( + manager: EntityManager, + service: Service, + oldTypeId?: string + ): Promise { + const newTypeId = service.serviceType?.id; + const typeChanged = oldTypeId !== newTypeId; + + if (service.status !== Status.Active) { + await RoomServicesModel.removeServiceFromAllRooms(manager, service.id); + return; + } + + if (!typeChanged) { + return; + } + + await RoomServicesModel.removeServiceFromAllRooms(manager, service.id); + + if (!newTypeId) { + return; + } + + const reloaded = await ServiceModel.list({ "Service.id": service.id }) + .then((q) => q.leftJoinAndSelect("Service.serviceType", "serviceType").getOne()); + + if (reloaded) { + await RoomServicesModel.syncRoomsOnServiceCreated(manager, reloaded); + } + } + + static async syncRoomsOnServiceDeleted( + manager: EntityManager, + serviceId: string + ): Promise { + await RoomServicesModel.removeServiceFromAllRooms(manager, serviceId); + } } diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 831565b..ea55d35 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -165,7 +165,7 @@ const doc = { $status: { "@enum": ["Y", "N"] }, }, roomServices: { - $service_ids: ["uuid-string"], + $service_type_ids: ["uuid-string"], }, serviceFare: { $service_id: "uuid-string",