fix: sửa lỗi hiển thị logo trên trang pdf

This commit is contained in:
2026-06-20 17:02:26 +07:00
parent e34d197dd0
commit ae97f061e8
21 changed files with 978 additions and 328 deletions
+83 -16
View File
@@ -516,11 +516,14 @@ let PublicTourController = class PublicTourController {
expenses: {
include: {
location: { select: { name: true, plannedStart: true } },
paidBy: { select: { name: true } }
paidBy: { select: { id: true, name: true } }
}
},
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
},
},
@@ -839,7 +842,10 @@ let TourController = class TourController {
orderBy: { sequence: 'asc' },
include: {
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
}
}
@@ -871,11 +877,14 @@ let TourController = class TourController {
expenses: {
include: {
location: { select: { name: true, plannedStart: true } },
paidBy: { select: { name: true } }
paidBy: { select: { id: true, name: true } }
}
},
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
},
},
@@ -931,6 +940,44 @@ let TourController = class TourController {
include: { user: { select: { id: true, name: true, email: true } } },
});
}
async updateMemberCounts(tourId, userId, body, req) {
const isSelf = req.user.id === userId;
const requesterParticipation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: req.user.id } },
});
const canManage = requesterParticipation?.role === 'OWNER' || requesterParticipation?.role === 'MANAGER';
if (!canManage && !isSelf) {
throw new common_1.ForbiddenException('Bạn không có quyền chỉnh sửa thông tin thành viên này.');
}
const data = {};
if (body.adultCount !== undefined) {
if (body.adultCount < 1)
throw new common_1.BadRequestException('Số lượng người lớn tối thiểu là 1.');
data.adultCount = Number(body.adultCount);
}
if (body.childCount !== undefined) {
if (body.childCount < 0)
throw new common_1.BadRequestException('Số lượng trẻ em không được âm.');
data.childCount = Number(body.childCount);
}
if (body.role !== undefined && canManage) {
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'];
if (validRoles.includes(body.role)) {
data.role = body.role;
}
}
await Promise.all([
this.cacheManager.del(`user-role:${userId}:${tourId}`),
this.cacheManager.del(tourId),
this.cacheManager.del(`/api/v1/tours/${tourId}`),
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
]);
return this.prisma.tourParticipant.update({
where: { tourId_userId: { tourId, userId } },
data,
include: { user: { select: { id: true, name: true, email: true } } },
});
}
async getJoinRequests(tourId, req) {
const requests = await this.prisma.joinRequest.findMany({
where: { tourId, status: 'PENDING' },
@@ -1248,6 +1295,18 @@ __decorate([
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise)
], TourController.prototype, "addMember", null);
__decorate([
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
(0, common_1.Patch)(':tourId/members/:userId'),
__param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Param)('userId', common_1.ParseUUIDPipe)),
__param(2, (0, common_1.Body)()),
__param(3, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, Object, Object]),
__metadata("design:returntype", Promise)
], TourController.prototype, "updateMemberCounts", null);
__decorate([
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
@@ -1515,7 +1574,10 @@ let RoutingController = class RoutingController {
tourId: currentLeg.tourId,
sequence: currentLeg.sequence - 1
},
include: { locations: { orderBy: { plannedStart: 'asc' } } }
include: { locations: { orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
] } }
});
if (prevLeg?.locations?.length) {
startAnchor = prevLeg.locations[prevLeg.locations.length - 1];
@@ -1568,7 +1630,10 @@ let RoutingController = class RoutingController {
})));
const updatedLocations = await this.prisma.location.findMany({
where: { legId },
orderBy: { plannedStart: 'asc' }
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
]
});
if (req.tourId) {
await Promise.all([
@@ -1820,14 +1885,17 @@ let UserController = class UserController {
async getAllUsers(req, q) {
const currentUserId = req.user?.id;
const users = await this.prisma.user.findMany({
where: q
? {
OR: [
{ name: { contains: q, mode: 'insensitive' } },
{ email: { contains: q, mode: 'insensitive' } },
],
}
: undefined,
where: {
isAnonymous: false,
...(q
? {
OR: [
{ name: { contains: q, mode: 'insensitive' } },
{ email: { contains: q, mode: 'insensitive' } },
],
}
: {}),
},
select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, createdAt: true, phone: true, address: true }
});
return users.filter((u) => u.id !== currentUserId);
@@ -1911,7 +1979,6 @@ let UserController = class UserController {
};
__decorate([
(0, common_1.Get)(),
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
__param(0, (0, common_1.Req)()),
__param(1, (0, common_1.Query)('q')),
__metadata("design:type", Function),
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Location" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "TourParticipant" ADD COLUMN "adultCount" INTEGER NOT NULL DEFAULT 1,
ADD COLUMN "childCount" INTEGER NOT NULL DEFAULT 0;
+9 -3
View File
@@ -118,14 +118,18 @@ model JoinRequest {
}
model TourParticipant {
id String @id @default(uuid())
tourId String
userId String
userId String?
role ParticipantRole @default(MEMBER)
displayName String?
adultCount Int @default(1)
childCount Int @default(0)
tour Tour @relation(fields: [tourId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([tourId, userId])
@@unique([tourId, userId])
}
model Leg {
@@ -162,6 +166,8 @@ model Location {
expenses Expense[]
photos Photo[]
comments Comment[]
createdAt DateTime @default(now())
}
model Expense {
+85 -16
View File
@@ -498,11 +498,14 @@ class PublicTourController {
expenses: {
include: {
location: { select: { name: true, plannedStart: true } },
paidBy: { select: { name: true } }
paidBy: { select: { id: true, name: true } }
}
},
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
},
},
@@ -896,7 +899,10 @@ class TourController {
orderBy: { sequence: 'asc' },
include: {
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
}
}
@@ -933,11 +939,14 @@ class TourController {
expenses: {
include: {
location: { select: { name: true, plannedStart: true } },
paidBy: { select: { name: true } }
paidBy: { select: { id: true, name: true } }
}
},
locations: {
orderBy: { plannedStart: 'asc' },
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
],
include: { _count: { select: { comments: true } } }
},
},
@@ -1003,6 +1012,58 @@ class TourController {
});
}
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY)
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Patch(':tourId/members/:userId')
async updateMemberCounts(
@Param('tourId', ParseUUIDPipe) tourId: string,
@Param('userId', ParseUUIDPipe) userId: string,
@Body() body: { adultCount?: number; childCount?: number; role?: string },
@Req() req: any
) {
const isSelf = req.user.id === userId;
// Tìm quyền hạn của người gửi yêu cầu trong tour này
const requesterParticipation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: req.user.id } },
});
const canManage = requesterParticipation?.role === 'OWNER' || requesterParticipation?.role === 'MANAGER';
if (!canManage && !isSelf) {
throw new ForbiddenException('Bạn không có quyền chỉnh sửa thông tin thành viên này.');
}
const data: any = {};
if (body.adultCount !== undefined) {
if (body.adultCount < 1) throw new BadRequestException('Số lượng người lớn tối thiểu là 1.');
data.adultCount = Number(body.adultCount);
}
if (body.childCount !== undefined) {
if (body.childCount < 0) throw new BadRequestException('Số lượng trẻ em không được âm.');
data.childCount = Number(body.childCount);
}
if (body.role !== undefined && canManage) {
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
if (validRoles.includes(body.role as any)) {
data.role = body.role;
}
}
// Xóa cache liên quan
await Promise.all([
this.cacheManager.del(`user-role:${userId}:${tourId}`),
this.cacheManager.del(tourId),
this.cacheManager.del(`/api/v1/tours/${tourId}`),
this.cacheManager.del(`/api/v1/tours/${tourId}/public`),
]);
return this.prisma.tourParticipant.update({
where: { tourId_userId: { tourId, userId } },
data,
include: { user: { select: { id: true, name: true, email: true } } },
});
}
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Only owner/manager can view join requests
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Get(':tourId/join-requests')
@@ -1449,7 +1510,10 @@ class RoutingController {
tourId: currentLeg.tourId,
sequence: currentLeg.sequence - 1
},
include: { locations: { orderBy: { plannedStart: 'asc' } } }
include: { locations: { orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
] } }
});
if (prevLeg?.locations?.length) {
@@ -1526,7 +1590,10 @@ class RoutingController {
const updatedLocations = await this.prisma.location.findMany({
where: { legId },
orderBy: { plannedStart: 'asc' }
orderBy: [
{ plannedStart: { sort: 'asc', nulls: 'last' } },
{ createdAt: 'asc' }
]
});
if (req.tourId) {
@@ -1779,18 +1846,20 @@ class UserController {
constructor(private prisma: PrismaService) {}
@Get()
@UseGuards(AdminGuard)
async getAllUsers(@Req() req: any, @Query('q') q?: string) {
const currentUserId = req.user?.id;
const users = await this.prisma.user.findMany({
where: q
? {
OR: [
{ name: { contains: q, mode: 'insensitive' as any } },
{ email: { contains: q, mode: 'insensitive' as any } },
],
}
: undefined,
where: {
isAnonymous: false,
...(q
? {
OR: [
{ name: { contains: q, mode: 'insensitive' as any } },
{ email: { contains: q, mode: 'insensitive' as any } },
],
}
: {}),
},
select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, createdAt: true, phone: true, address: true }
});
return users.filter((u: any) => u.id !== currentUserId);