fix: sửa tính năng hiển thị quãng đường tại điểm đến kế tiếp
This commit is contained in:
1 parent
7ad785fed9
commit
c5474f1ff6
7 files changed
+450
-107
No files matched your search
Vendored
+117
-16
@@ -189,6 +189,9 @@ let TourRoleGuard = class TourRoleGuard {
|
||||
if (!rolesToCheck.some(r => role === r)) {
|
||||
throw new common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
|
||||
}
|
||||
if (tourId) {
|
||||
request.tourId = tourId;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -405,6 +408,11 @@ let TourController = class TourController {
|
||||
}
|
||||
});
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
@@ -432,6 +440,13 @@ let TourController = class TourController {
|
||||
legId: firstLeg.id,
|
||||
plannedStart: new Date(0),
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
async updateTourEndPoint(tourId, body, req) {
|
||||
@@ -458,6 +473,13 @@ let TourController = class TourController {
|
||||
legId: lastLeg.id,
|
||||
plannedEnd: new Date(0),
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
async initializeLegs(tourId, body) {
|
||||
@@ -491,6 +513,11 @@ let TourController = class TourController {
|
||||
data: { legId: lastLeg.id }
|
||||
});
|
||||
}
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return allLegs;
|
||||
}
|
||||
async addLeg(tourId, body) {
|
||||
@@ -506,6 +533,13 @@ let TourController = class TourController {
|
||||
sequence: tour.legs.length + 1,
|
||||
note: body.note || `Chặng ${tour.legs.length + 1}`
|
||||
}
|
||||
}).then(async (leg) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return leg;
|
||||
});
|
||||
}
|
||||
async updateTour(id, body) {
|
||||
@@ -521,6 +555,13 @@ let TourController = class TourController {
|
||||
childCount: body.childCount !== undefined ? Number(body.childCount) : undefined,
|
||||
childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined,
|
||||
},
|
||||
}).then(async (tour) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(id),
|
||||
this.cacheManager.del(`/api/v1/tours/${id}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${id}/public`),
|
||||
]);
|
||||
return tour;
|
||||
});
|
||||
}
|
||||
async deleteTour(id) {
|
||||
@@ -1013,10 +1054,11 @@ TourController = __decorate([
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
|
||||
], TourController);
|
||||
let LocationController = class LocationController {
|
||||
constructor(prisma) {
|
||||
constructor(prisma, cacheManager) {
|
||||
this.prisma = prisma;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
async updateLocation(id, body) {
|
||||
async updateLocation(id, body, req) {
|
||||
const { expenseAmount, expenseCategory, ...data } = body;
|
||||
const location = await this.prisma.location.update({
|
||||
where: { id },
|
||||
@@ -1054,38 +1096,63 @@ let LocationController = class LocationController {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
async deleteLocation(id) {
|
||||
await this.prisma.location.delete({ where: { id } });
|
||||
async deleteLocation(id, req) {
|
||||
try {
|
||||
await this.prisma.location.delete({ where: { id } });
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
await this.cacheManager.del(`res-to-tour:${id}`);
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
__decorate([
|
||||
(0, common_1.Patch)(':id'),
|
||||
(0, common_1.UseGuards)(TourRoleGuard),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||
__param(1, (0, common_1.Body)()),
|
||||
__param(2, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [String, Object]),
|
||||
__metadata("design:paramtypes", [String, Object, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], LocationController.prototype, "updateLocation", null);
|
||||
__decorate([
|
||||
(0, common_1.Delete)(':id'),
|
||||
(0, common_1.UseGuards)(TourRoleGuard),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||
__param(1, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
__metadata("design:paramtypes", [String, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], LocationController.prototype, "deleteLocation", null);
|
||||
LocationController = __decorate([
|
||||
(0, common_1.Controller)('locations'),
|
||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
__param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
|
||||
], LocationController);
|
||||
let LegController = class LegController {
|
||||
constructor(prisma) {
|
||||
constructor(prisma, cacheManager) {
|
||||
this.prisma = prisma;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
async updateLeg(id, body) {
|
||||
async updateLeg(id, body, req) {
|
||||
return this.prisma.leg.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -1095,6 +1162,13 @@ let LegController = class LegController {
|
||||
endDate: body.endDate ? new Date(body.endDate) : undefined,
|
||||
description: body.description,
|
||||
}
|
||||
}).then(async (leg) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
return leg;
|
||||
});
|
||||
}
|
||||
async deleteLeg(id) {
|
||||
@@ -1105,20 +1179,35 @@ let LegController = class LegController {
|
||||
if (leg?._count.locations && leg._count.locations > 0) {
|
||||
throw new common_1.BadRequestException('Không thể xóa chặng đang có địa điểm. Hãy xóa địa điểm trước.');
|
||||
}
|
||||
await this.prisma.leg.delete({ where: { id } });
|
||||
try {
|
||||
const deletedLeg = await this.prisma.leg.delete({ where: { id } });
|
||||
await this.cacheManager.del(`res-to-tour:${id}`);
|
||||
if (deletedLeg.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(deletedLeg.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
__decorate([
|
||||
(0, common_1.Patch)(':id'),
|
||||
(0, common_1.UseGuards)(TourRoleGuard),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||
__param(1, (0, common_1.Body)()),
|
||||
__param(2, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [String, Object]),
|
||||
__metadata("design:paramtypes", [String, Object, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], LegController.prototype, "updateLeg", null);
|
||||
__decorate([
|
||||
(0, common_1.Delete)(':id'),
|
||||
(0, common_1.UseGuards)(TourRoleGuard),
|
||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
@@ -1128,7 +1217,8 @@ LegController = __decorate([
|
||||
(0, common_1.Controller)('legs'),
|
||||
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
__param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
|
||||
], LegController);
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const p = 0.017453292519943295;
|
||||
@@ -1139,10 +1229,11 @@ function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
return 12742 * Math.asin(Math.sqrt(a));
|
||||
}
|
||||
let RoutingController = class RoutingController {
|
||||
constructor(prisma) {
|
||||
constructor(prisma, cacheManager) {
|
||||
this.prisma = prisma;
|
||||
this.cacheManager = cacheManager;
|
||||
}
|
||||
async optimize(legId) {
|
||||
async optimize(legId, req) {
|
||||
const currentLeg = await this.prisma.leg.findUnique({
|
||||
where: { id: legId },
|
||||
});
|
||||
@@ -1214,6 +1305,13 @@ let RoutingController = class RoutingController {
|
||||
where: { legId },
|
||||
orderBy: { plannedStart: 'asc' }
|
||||
});
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
locations: updatedLocations,
|
||||
totalDistance: parseFloat(totalDistance.toFixed(2))
|
||||
@@ -1222,16 +1320,19 @@ let RoutingController = class RoutingController {
|
||||
};
|
||||
__decorate([
|
||||
(0, common_1.Post)('optimize/:legId'),
|
||||
(0, common_1.UseGuards)(TourRoleGuard),
|
||||
__param(0, (0, common_1.Param)('legId', common_1.ParseUUIDPipe)),
|
||||
__param(1, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [String]),
|
||||
__metadata("design:paramtypes", [String, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], RoutingController.prototype, "optimize", null);
|
||||
RoutingController = __decorate([
|
||||
(0, common_1.Controller)('routing'),
|
||||
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
__param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
|
||||
], RoutingController);
|
||||
let PhotoController = class PhotoController {
|
||||
constructor(prisma) {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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 truncated
|
||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"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 truncated
|
||||
+112
-11
@@ -178,6 +178,11 @@ export class TourRoleGuard implements CanActivate {
|
||||
if (!rolesToCheck.some(r => role === r)) {
|
||||
throw new ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
|
||||
}
|
||||
|
||||
// Ensure tourId is attached to the request object for controllers to use
|
||||
if (tourId) {
|
||||
(request as any).tourId = tourId;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -350,6 +355,7 @@ class TourController {
|
||||
if (!leg) throw new NotFoundException('Không tìm thấy chặng phù hợp để thêm địa điểm');
|
||||
|
||||
return this.prisma.location.create({
|
||||
// ...
|
||||
data: {
|
||||
name: body.name,
|
||||
address: body.address,
|
||||
@@ -374,7 +380,13 @@ class TourController {
|
||||
}
|
||||
});
|
||||
}
|
||||
return loc;
|
||||
// Xóa triệt để các loại cache của Tour (cả key UUID và key URL của Interceptor)
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc; // Return the created location
|
||||
});
|
||||
}
|
||||
|
||||
@@ -413,6 +425,13 @@ class TourController {
|
||||
legId: firstLeg.id,
|
||||
plannedStart: new Date(0),
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -449,6 +468,13 @@ class TourController {
|
||||
legId: lastLeg.id,
|
||||
plannedEnd: new Date(0), // Đánh dấu đây là điểm kết thúc đặc biệt
|
||||
}
|
||||
}).then(async (loc) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return loc;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -495,7 +521,13 @@ class TourController {
|
||||
});
|
||||
}
|
||||
|
||||
return allLegs;
|
||||
// Invalidate cache for the tour after initializing legs
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return allLegs; // Return all legs
|
||||
}
|
||||
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can add legs
|
||||
@@ -514,6 +546,13 @@ class TourController {
|
||||
sequence: tour.legs.length + 1,
|
||||
note: body.note || `Chặng ${tour.legs.length + 1}`
|
||||
}
|
||||
}).then(async (leg) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
|
||||
]);
|
||||
return leg;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -534,6 +573,13 @@ class TourController {
|
||||
childCount: body.childCount !== undefined ? Number(body.childCount) : undefined,
|
||||
childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined,
|
||||
},
|
||||
}).then(async (tour) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(id),
|
||||
this.cacheManager.del(`/api/v1/tours/${id}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${id}/public`),
|
||||
]);
|
||||
return tour;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -938,10 +984,11 @@ class TourController {
|
||||
@Controller('locations')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
class LocationController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
@Patch(':id')
|
||||
async updateLocation(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
|
||||
@UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
|
||||
async updateLocation(@Param('id', ParseUUIDPipe) id: string, @Body() body: any, @Req() req: any) {
|
||||
const { expenseAmount, expenseCategory, ...data } = body;
|
||||
|
||||
const location = await this.prisma.location.update({
|
||||
@@ -982,12 +1029,35 @@ class LocationController {
|
||||
}
|
||||
}
|
||||
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteLocation(@Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.prisma.location.delete({ where: { id } });
|
||||
@UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
|
||||
async deleteLocation(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
|
||||
try {
|
||||
await this.prisma.location.delete({ where: { id } });
|
||||
} catch (e) {
|
||||
// Nếu bản ghi đã bị xóa trước đó, không ném lỗi 500 để đảm bảo tính an toàn (idempotency)
|
||||
}
|
||||
|
||||
// Xóa mapping cache để Guard không bị đánh lừa ở lần truy cập sau
|
||||
await this.cacheManager.del(`res-to-tour:${id}`);
|
||||
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -996,10 +1066,11 @@ class LocationController {
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Leg operations
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
class LegController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
@Patch(':id')
|
||||
async updateLeg(@Param('id', ParseUUIDPipe) id: string, @Body() body: any) {
|
||||
@UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
|
||||
async updateLeg(@Param('id', ParseUUIDPipe) id: string, @Body() body: any, @Req() req: any) {
|
||||
return this.prisma.leg.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -1009,10 +1080,18 @@ class LegController {
|
||||
endDate: body.endDate ? new Date(body.endDate) : undefined,
|
||||
description: body.description,
|
||||
}
|
||||
}).then(async (leg) => {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
return leg;
|
||||
});
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
|
||||
async deleteLeg(@Param('id', ParseUUIDPipe) id: string) {
|
||||
const leg = await this.prisma.leg.findUnique({
|
||||
where: { id },
|
||||
@@ -1023,7 +1102,21 @@ class LegController {
|
||||
throw new BadRequestException('Không thể xóa chặng đang có địa điểm. Hãy xóa địa điểm trước.');
|
||||
}
|
||||
|
||||
await this.prisma.leg.delete({ where: { id } });
|
||||
try {
|
||||
const deletedLeg = await this.prisma.leg.delete({ where: { id } });
|
||||
await this.cacheManager.del(`res-to-tour:${id}`);
|
||||
|
||||
if (deletedLeg.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(deletedLeg.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${deletedLeg.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Idempotency
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -1044,10 +1137,11 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
|
||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Routing operations
|
||||
@UseGuards(JwtAuthGuard, TourRoleGuard)
|
||||
class RoutingController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(private prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
@Post('optimize/:legId')
|
||||
async optimize(@Param('legId', ParseUUIDPipe) legId: string) {
|
||||
@UseGuards(TourRoleGuard) // Apply TourRoleGuard to get tourId
|
||||
async optimize(@Param('legId', ParseUUIDPipe) legId: string, @Req() req: any) {
|
||||
const currentLeg = await this.prisma.leg.findUnique({
|
||||
where: { id: legId },
|
||||
});
|
||||
@@ -1148,6 +1242,13 @@ class RoutingController {
|
||||
orderBy: { plannedStart: 'asc' }
|
||||
});
|
||||
|
||||
if (req.tourId) {
|
||||
await Promise.all([
|
||||
this.cacheManager.del(req.tourId),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}`),
|
||||
this.cacheManager.del(`/api/v1/tours/${req.tourId}/public`),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
locations: updatedLocations,
|
||||
totalDistance: parseFloat(totalDistance.toFixed(2))
|
||||
|
||||
Reference in new issue
Block a user