fix: admin empty thùng rác

This commit is contained in:
2026-06-22 12:28:21 +07:00
parent 860395cb14
commit 01d6d7439f
54 changed files with 992 additions and 361 deletions
+4
View File
@@ -1,4 +1,8 @@
declare const JwtAuthGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>; declare const JwtAuthGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
export declare class JwtAuthGuard extends JwtAuthGuard_base { export declare class JwtAuthGuard extends JwtAuthGuard_base {
} }
declare const JwtAuthGuardNoAnonymous_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
export declare class JwtAuthGuardNoAnonymous extends JwtAuthGuardNoAnonymous_base {
handleRequest(err: any, user: any, info: any): any;
}
export {}; export {};
+17 -1
View File
@@ -6,13 +6,29 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
return c > 3 && r && Object.defineProperty(target, key, r), r; return c > 3 && r && Object.defineProperty(target, key, r), r;
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.JwtAuthGuard = void 0; exports.JwtAuthGuardNoAnonymous = exports.JwtAuthGuard = void 0;
const common_1 = require("@nestjs/common"); const common_1 = require("@nestjs/common");
const passport_1 = require("@nestjs/passport"); const passport_1 = require("@nestjs/passport");
const common_2 = require("@nestjs/common");
let JwtAuthGuard = class JwtAuthGuard extends (0, passport_1.AuthGuard)('jwt') { let JwtAuthGuard = class JwtAuthGuard extends (0, passport_1.AuthGuard)('jwt') {
}; };
exports.JwtAuthGuard = JwtAuthGuard; exports.JwtAuthGuard = JwtAuthGuard;
exports.JwtAuthGuard = JwtAuthGuard = __decorate([ exports.JwtAuthGuard = JwtAuthGuard = __decorate([
(0, common_1.Injectable)() (0, common_1.Injectable)()
], JwtAuthGuard); ], JwtAuthGuard);
let JwtAuthGuardNoAnonymous = class JwtAuthGuardNoAnonymous extends (0, passport_1.AuthGuard)('jwt') {
handleRequest(err, user, info) {
if (err || !user) {
throw err || new common_2.UnauthorizedException('Phiên làm việc không hợp lệ hoặc đã hết hạn');
}
if (user.isAnonymous) {
throw new common_2.UnauthorizedException('Tài khoản khách không có quyền truy cập tính năng này. Vui lòng đăng ký tài khoản để tiếp tục.');
}
return user;
}
};
exports.JwtAuthGuardNoAnonymous = JwtAuthGuardNoAnonymous;
exports.JwtAuthGuardNoAnonymous = JwtAuthGuardNoAnonymous = __decorate([
(0, common_1.Injectable)()
], JwtAuthGuardNoAnonymous);
//# sourceMappingURL=jwt-auth.guard.js.map //# sourceMappingURL=jwt-auth.guard.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"jwt-auth.guard.js","sourceRoot":"","sources":["../../../src/auth/jwt-auth.guard.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAC5C,+CAA6C;AAGtC,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,IAAA,oBAAS,EAAC,KAAK,CAAC;CAAG,CAAA;AAAxC,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;GACA,YAAY,CAA4B"} {"version":3,"file":"jwt-auth.guard.js","sourceRoot":"","sources":["../../../src/auth/jwt-auth.guard.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAC5C,+CAA6C;AAC7C,2CAAuD;AAGhD,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,IAAA,oBAAS,EAAC,KAAK,CAAC;CAAG,CAAA;AAAxC,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;GACA,YAAY,CAA4B;AAI9C,IAAM,uBAAuB,GAA7B,MAAM,uBAAwB,SAAQ,IAAA,oBAAS,EAAC,KAAK,CAAC;IAC3D,aAAa,CAAC,GAAQ,EAAE,IAAS,EAAE,IAAS;QAC1C,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,GAAG,IAAI,IAAI,8BAAqB,CAAC,6CAA6C,CAAC,CAAC;QACxF,CAAC;QAGD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,IAAI,8BAAqB,CAAC,gGAAgG,CAAC,CAAC;QACpI,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAbY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;GACA,uBAAuB,CAanC"}
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAmE;AACnE,+CAAoD;AACpD,+CAAoD;AACpD,gEAA4D;AAGrD,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IACzD,YAAoB,MAAqB;QACvC,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,cAAc;SACtD,CAAC,CAAC;QALe,WAAM,GAAN,MAAM,CAAe;IAMzC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAY;QACzB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,8BAAqB,CAAC,sDAAsD,CAAC,CAAC;QAC1F,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AApBY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,WAAW,CAoBvB"} {"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAmE;AACnE,+CAAoD;AACpD,+CAAoD;AACpD,gEAA4D;AAGrD,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IACzD,YAAoB,MAAqB;QACvC,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,cAAc;SACtD,CAAC,CAAC;QALe,WAAM,GAAN,MAAM,CAAe;IAMzC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAY;QACzB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;YAC7C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,8BAAqB,CAAC,sDAAsD,CAAC,CAAC;QAC1F,CAAC;QAID,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAtBY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,WAAW,CAsBvB"}
+377 -39
View File
@@ -71,6 +71,7 @@ const admin_guard_1 = require("./auth/admin.guard");
const nodemailer = __importStar(require("nodemailer")); const nodemailer = __importStar(require("nodemailer"));
const jwt_1 = require("@nestjs/jwt"); const jwt_1 = require("@nestjs/jwt");
const jwt_auth_guard_1 = require("./auth/jwt-auth.guard"); const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
const jwt_auth_guard_2 = require("./auth/jwt-auth.guard");
const jwt_strategy_1 = require("./auth/jwt.strategy"); const jwt_strategy_1 = require("./auth/jwt.strategy");
const core_2 = require("@nestjs/core"); const core_2 = require("@nestjs/core");
const common_2 = require("@nestjs/common"); const common_2 = require("@nestjs/common");
@@ -823,6 +824,34 @@ let TourController = class TourController {
} }
} }
}); });
const noteContent = `<h2>${filteredTitle} - Initial Planning</h2>
<p><strong>Start Date:</strong> ${startDate ? new Date(startDate).toLocaleDateString() : 'TBD'}</p>
<p><strong>End Date:</strong> ${endDate ? new Date(endDate).toLocaleDateString() : 'TBD'}</p>
<p><strong>Adult Participants:</strong> ${adultCount || 1}</p>
<p><strong>Child Participants:</strong> ${childCount || 0}</p>
<h3>Key Items to Plan:</h3>
<ul>
<li>Accommodations</li>
<li>Transportation</li>
<li>Activities & Attractions</li>
<li>Budget & Expenses</li>
<li>Important Contact Numbers</li>
<li>Special Requirements & Notes</li>
</ul>
<p><em>Add your planning notes here...</em></p>`;
try {
await this.prisma.tourNote.create({
data: {
tourId: tour.id,
userId: req.user.id,
title: `[${filteredTitle}] - Initial Planning`,
content: noteContent
}
});
}
catch (err) {
console.warn(`Failed to auto-create note for tour ${tour.id}:`, err.message);
}
await this.cacheManager.del(`/api/v1/tours/explore`); await this.cacheManager.del(`/api/v1/tours/explore`);
return tour; return tour;
} }
@@ -1464,15 +1493,37 @@ let TourController = class TourController {
console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message); console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message);
} }
} }
await (0, sharp_1.default)(processBuffer) const uploadsDir = path.dirname(displayFilePath);
.rotate() if (!fs.existsSync(uploadsDir)) {
.resize(2560, 2560, { try {
fit: 'inside', fs.mkdirSync(uploadsDir, { recursive: true });
withoutEnlargement: true console.log(`[Photo] Created directory: ${uploadsDir}`);
}) }
.jpeg({ quality: 85 }) catch (e) {
.toFile(displayFilePath); console.error(`[Photo] Failed to create directory ${uploadsDir}:`, e);
return this.prisma.photo.create({ throw new common_1.BadRequestException(`Lỗi tạo thư mục: ${e.message}`);
}
}
try {
await (0, sharp_1.default)(processBuffer)
.rotate()
.resize(2560, 2560, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({ quality: 85 })
.toFile(displayFilePath);
console.log(`[Photo] Display image saved successfully: ${displayFilePath}`);
}
catch (e) {
console.error(`[Photo] Failed to save display image to ${displayFilePath}:`, e);
throw new common_1.BadRequestException(`Lỗi lưu hình ảnh: ${e.message}`);
}
if (!fs.existsSync(displayFilePath)) {
console.error(`[Photo] File verification failed at ${displayFilePath}`);
throw new common_1.BadRequestException('Lỗi: Hình ảnh không được lưu thành công. Vui lòng kiểm tra quyền lưu trữ.');
}
const photoRecord = await this.prisma.photo.create({
data: { data: {
tourId: tourId, tourId: tourId,
uploaderId: uploaderId, uploaderId: uploaderId,
@@ -1485,6 +1536,8 @@ let TourController = class TourController {
} }
} }
}); });
console.log(`[Photo] Database record created: ID=${photoRecord.id}, imageUrl=${photoRecord.imageUrl}`);
return photoRecord;
})); }));
} }
async createInvitation(tourId, body, req) { async createInvitation(tourId, body, req) {
@@ -1528,17 +1581,31 @@ let TourController = class TourController {
} }
async joinByToken(body, req) { async joinByToken(body, req) {
const { token } = body; const { token } = body;
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
if (!token) { if (!token) {
console.error('[joinByToken] No token provided');
throw new common_1.BadRequestException('Vui lòng cung cấp token lời mời.'); throw new common_1.BadRequestException('Vui lòng cung cấp token lời mời.');
} }
console.log('[joinByToken] Searching for invitation with token:', token.substring(0, 20) + '...');
const invitation = await this.prisma.tourInvitation.findUnique({ const invitation = await this.prisma.tourInvitation.findUnique({
where: { token }, where: { token },
include: { tour: { select: { title: true } } } include: { tour: { select: { id: true, title: true } } }
}); });
if (!invitation) { if (!invitation) {
console.error('[joinByToken] Invitation not found for token:', token.substring(0, 20) + '...');
const totalInvitations = await this.prisma.tourInvitation.count();
console.log('[joinByToken] Total invitations in database:', totalInvitations);
throw new common_1.NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.'); throw new common_1.NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
} }
const userEmail = req.user.email;
console.log('[joinByToken] Checking email - User:', userEmail, 'Invitation:', invitation.email);
if (invitation.email && userEmail && invitation.email !== userEmail) {
console.error('[joinByToken] Email mismatch - User email:', userEmail, 'Invitation email:', invitation.email);
throw new common_1.BadRequestException('Lời mời này được gửi tới một địa chỉ email khác. Vui lòng đăng nhập bằng tài khoản đúng.');
}
console.log('[joinByToken] Invitation found, tourId:', invitation.tourId, 'expired:', invitation.expiredAt < new Date());
if (invitation.expiredAt < new Date()) { if (invitation.expiredAt < new Date()) {
console.error('[joinByToken] Invitation expired at:', invitation.expiredAt);
try { try {
await this.prisma.tourInvitation.delete({ where: { id: invitation.id } }); await this.prisma.tourInvitation.delete({ where: { id: invitation.id } });
} }
@@ -1546,10 +1613,12 @@ let TourController = class TourController {
throw new common_1.BadRequestException('Lời mời đã hết hạn.'); throw new common_1.BadRequestException('Lời mời đã hết hạn.');
} }
const userId = req.user.id; const userId = req.user.id;
console.log('[joinByToken] User attempting to join - userId:', userId, 'tourId:', invitation.tourId);
const existingParticipation = await this.prisma.tourParticipant.findUnique({ const existingParticipation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId: invitation.tourId, userId } } where: { tourId_userId: { tourId: invitation.tourId, userId } }
}); });
if (existingParticipation) { if (existingParticipation) {
console.log('[joinByToken] User is already a participant');
try { try {
await this.prisma.tourInvitation.delete({ where: { id: invitation.id } }); await this.prisma.tourInvitation.delete({ where: { id: invitation.id } });
} }
@@ -1582,6 +1651,7 @@ let TourController = class TourController {
where: { id: invitation.id } where: { id: invitation.id }
}) })
]); ]);
console.log('[joinByToken] Successfully joined tour:', invitation.tourId, 'userId:', userId);
return { success: true, tourId: invitation.tourId, message: `Bạn đã gia nhập hành trình "${invitation.tour.title}"!`, shouldRefresh: true }; return { success: true, tourId: invitation.tourId, message: `Bạn đã gia nhập hành trình "${invitation.tour.title}"!`, shouldRefresh: true };
} }
async mergeMember(tourId, body, req) { async mergeMember(tourId, body, req) {
@@ -1709,7 +1779,7 @@ __decorate([
], TourController.prototype, "deleteTour", null); ], TourController.prototype, "deleteTour", null);
__decorate([ __decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor), (0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard), (0, common_1.UseGuards)(jwt_auth_guard_2.JwtAuthGuardNoAnonymous),
(0, common_1.Get)('explore'), (0, common_1.Get)('explore'),
__param(0, (0, common_1.Req)()), __param(0, (0, common_1.Req)()),
__metadata("design:type", Function), __metadata("design:type", Function),
@@ -2155,11 +2225,15 @@ let PhotoController = class PhotoController {
const originalFilename = `${uniqueSuffix}${originalExtension}`; const originalFilename = `${uniqueSuffix}${originalExtension}`;
const displayFilename = `${uniqueSuffix}.jpg`; const displayFilename = `${uniqueSuffix}.jpg`;
const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals'); const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals');
const displayFilePath = path.join(UPLOAD_ROOT, 'tours', displayFilename); const tourDisplayPath = path.join(UPLOAD_ROOT, 'tours');
const displayFilePath = path.join(tourDisplayPath, displayFilename);
const originalFilePath = path.join(memberOriginalDir, originalFilename); const originalFilePath = path.join(memberOriginalDir, originalFilename);
if (!fs.existsSync(memberOriginalDir)) { if (!fs.existsSync(memberOriginalDir)) {
fs.mkdirSync(memberOriginalDir, { recursive: true }); fs.mkdirSync(memberOriginalDir, { recursive: true });
} }
if (!fs.existsSync(tourDisplayPath)) {
fs.mkdirSync(tourDisplayPath, { recursive: true });
}
await fs.promises.writeFile(originalFilePath, file.buffer); await fs.promises.writeFile(originalFilePath, file.buffer);
let lat; let lat;
let lng; let lng;
@@ -3037,6 +3111,23 @@ let PublicPhotoController = class PublicPhotoController {
</html>`; </html>`;
res.type('text/html').send(htmlContent); res.type('text/html').send(htmlContent);
} }
async flagPhoto(photoId, body, req) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
const updatedPhoto = await this.prisma.photo.update({
where: { id: photoId },
data: {
isFlagged: true,
flaggedReason: body.reason || 'Không xác định',
flaggedAt: new Date()
}
});
return { success: true, message: 'Đã báo cáo ảnh thành công', photo: updatedPhoto };
}
}; };
__decorate([ __decorate([
(0, common_1.Get)(), (0, common_1.Get)(),
@@ -3079,6 +3170,16 @@ __decorate([
__metadata("design:paramtypes", [String, Object, Object]), __metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "sharePhoto", null); ], PublicPhotoController.prototype, "sharePhoto", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Post)(':photoId/flag'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Body)()),
__param(2, (0, common_1.Req)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object, Object]),
__metadata("design:returntype", Promise)
], PublicPhotoController.prototype, "flagPhoto", null);
PublicPhotoController = __decorate([ PublicPhotoController = __decorate([
(0, common_1.Controller)('public-photos'), (0, common_1.Controller)('public-photos'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService, __metadata("design:paramtypes", [prisma_service_1.PrismaService,
@@ -3631,6 +3732,79 @@ AdminModerationController = __decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService]) __metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminModerationController); ], AdminModerationController);
let AdminPhotosController = class AdminPhotosController {
constructor(prisma) {
this.prisma = prisma;
}
async getFlaggedPhotos() {
return this.prisma.photo.findMany({
where: { isFlagged: true },
include: {
uploader: { select: { id: true, name: true, email: true } },
tour: { select: { id: true, title: true } }
},
orderBy: { flaggedAt: 'desc' }
});
}
async approvePhoto(photoId) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
const updatedPhoto = await this.prisma.photo.update({
where: { id: photoId },
data: {
isFlagged: false,
flaggedReason: null,
flaggedAt: null
}
});
return { success: true, message: 'Đã duyệt ảnh thành công', photo: updatedPhoto };
}
async deletePhoto(photoId) {
const photo = await this.prisma.photo.findUnique({
where: { id: photoId }
});
if (!photo) {
throw new common_1.NotFoundException('Ảnh không tồn tại');
}
const deletedPhoto = await this.prisma.photo.update({
where: { id: photoId },
data: {
isDeleted: true,
deletedAt: new Date()
}
});
return { success: true, message: 'Đã xóa ảnh thành công', photo: deletedPhoto };
}
};
__decorate([
(0, common_1.Get)('flagged'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "getFlaggedPhotos", null);
__decorate([
(0, common_1.Post)(':photoId/approve'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "approvePhoto", null);
__decorate([
(0, common_1.Delete)(':photoId'),
__param(0, (0, common_1.Param)('photoId', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], AdminPhotosController.prototype, "deletePhoto", null);
AdminPhotosController = __decorate([
(0, common_1.Controller)('admin/photos'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], AdminPhotosController);
let ReportsController = class ReportsController { let ReportsController = class ReportsController {
constructor(prisma) { constructor(prisma) {
this.prisma = prisma; this.prisma = prisma;
@@ -4347,67 +4521,225 @@ let AdminTrashController = class AdminTrashController {
} }
async deletePermanentItems(body) { async deletePermanentItems(body) {
const { type, ids } = body; const { type, ids } = body;
console.log('[deletePermanent] Request received - type:', type, 'ids count:', ids.length);
if (!type || !ids || !Array.isArray(ids)) { if (!type || !ids || !Array.isArray(ids)) {
throw new common_1.BadRequestException('Tham số không hợp lệ.'); throw new common_1.BadRequestException('Tham số không hợp lệ.');
} }
const deleteResults = { success: 0, failed: 0, errors: [] };
if (type === 'tour') { if (type === 'tour') {
for (const tourId of ids) { for (const tourId of ids) {
const photos = await this.prisma.photo.findMany({ where: { tourId } }); try {
for (const p of photos) { console.log('[deletePermanent] Deleting tour:', tourId);
if (p.imageUrl) { const photos = await this.prisma.photo.findMany({ where: { tourId } });
const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, '')); console.log('[deletePermanent] Found', photos.length, 'photos for tour');
if (fs.existsSync(displayFilePath)) { for (const p of photos) {
try { if (p.imageUrl) {
fs.unlinkSync(displayFilePath); const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking display file:', displayFilePath);
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
console.log('[deletePermanent] Deleted display file:', displayFilePath);
}
catch (e) {
console.error('[deletePermanent] Error deleting display file:', e);
}
}
}
if (p.originalUrl) {
const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking original file:', originalFilePath);
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
console.log('[deletePermanent] Deleted original file:', originalFilePath);
}
catch (e) {
console.error('[deletePermanent] Error deleting original file:', e);
}
} }
catch (e) { }
}
}
if (p.originalUrl) {
const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, ''));
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
}
catch (e) { }
} }
} }
await this.prisma.tour.delete({ where: { id: tourId } });
console.log('[deletePermanent] Deleted tour from database:', tourId);
deleteResults.success++;
}
catch (e) {
console.error('[deletePermanent] Error deleting tour:', tourId, e);
deleteResults.failed++;
deleteResults.errors.push(`Tour ${tourId}: ${e?.message || 'Unknown error'}`);
} }
await this.prisma.tour.delete({ where: { id: tourId } });
} }
} }
else if (type === 'photo') { else if (type === 'photo') {
console.log('[deletePermanent] Deleting', ids.length, 'photos');
console.log('[deletePermanent] Photo IDs:', JSON.stringify(ids));
if (!ids || ids.length === 0) {
console.warn('[deletePermanent] No photo IDs provided');
return { success: true, deleted: 0, failed: 0 };
}
for (const photoId of ids) { for (const photoId of ids) {
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } }); try {
if (photo) { console.log('[deletePermanent] ========== START Processing photo:', photoId);
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
if (!photo) {
console.warn('[deletePermanent] Photo not found in DB:', photoId);
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: not found in database`);
console.log('[deletePermanent] ========== SKIP (not found):', photoId);
continue;
}
console.log('[deletePermanent] Found photo:', { id: photo.id, imageUrl: photo.imageUrl, isDeleted: photo.isDeleted, tourId: photo.tourId });
console.log('[deletePermanent] Deleting comments for photo:', photoId);
try {
const commentResult = await this.prisma.comment.deleteMany({
where: { photoId }
});
console.log('[deletePermanent] Deleted', commentResult.count, 'comments for photo');
}
catch (e) {
console.warn('[deletePermanent] Error deleting comments (continuing):', e?.message);
}
if (photo.imageUrl) { if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking display file:', displayFilePath);
if (fs.existsSync(displayFilePath)) { if (fs.existsSync(displayFilePath)) {
try { try {
fs.unlinkSync(displayFilePath); fs.unlinkSync(displayFilePath);
console.log('[deletePermanent] ✓ Deleted display file');
}
catch (e) {
console.error('[deletePermanent] Error deleting display file:', e);
} }
catch (e) { }
} }
} }
if (photo.originalUrl) { if (photo.originalUrl) {
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, '')); const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking original file:', originalFilePath);
if (fs.existsSync(originalFilePath)) { if (fs.existsSync(originalFilePath)) {
try { try {
fs.unlinkSync(originalFilePath); fs.unlinkSync(originalFilePath);
console.log('[deletePermanent] ✓ Deleted original file');
}
catch (e) {
console.error('[deletePermanent] Error deleting original file:', e);
} }
catch (e) { }
} }
} }
await this.prisma.photo.delete({ where: { id: photoId } }); console.log('[deletePermanent] Attempting database delete for photo:', photoId);
const result = await this.prisma.photo.delete({ where: { id: photoId } });
console.log('[deletePermanent] ✓ Successfully deleted photo from database:', photoId);
deleteResults.success++;
console.log('[deletePermanent] ========== SUCCESS:', photoId);
}
catch (e) {
console.error('[deletePermanent] ✗ Error deleting photo:', photoId);
console.error('[deletePermanent] Error details:', e?.message || String(e));
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: ${e?.message || 'Unknown error'}`);
console.log('[deletePermanent] ========== FAILED:', photoId);
} }
} }
console.log('[deletePermanent] Photos deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
} }
else if (type === 'note') { else if (type === 'note') {
await this.prisma.tourNote.deleteMany({ console.log('[deletePermanent] Deleting', ids.length, 'notes');
where: { id: { in: ids } } try {
}); const result = await this.prisma.tourNote.deleteMany({
where: { id: { in: ids } }
});
console.log('[deletePermanent] Deleted notes from database, count:', result.count);
deleteResults.success = result.count;
}
catch (e) {
console.error('[deletePermanent] Error deleting notes:', e);
deleteResults.failed = ids.length;
deleteResults.errors.push(`Notes deletion: ${e?.message || 'Unknown error'}`);
}
} }
return { success: true }; console.log('[deletePermanent] Deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
if (deleteResults.failed > 0) {
console.warn('[deletePermanent] Errors:', deleteResults.errors);
}
return {
success: true,
deleted: deleteResults.success,
failed: deleteResults.failed,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
}
async emptyAllTrash() {
console.log('[emptyAllTrash] Starting to empty all trash');
const deleteResults = { success: 0, failed: 0, errors: [] };
try {
console.log('[emptyAllTrash] Finding all deleted photos');
const deletedPhotos = await this.prisma.photo.findMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Found', deletedPhotos.length, 'deleted photos');
console.log('[emptyAllTrash] Deleting all comments from deleted photos');
const commentCount = await this.prisma.comment.deleteMany({
where: {
photoId: { in: deletedPhotos.map(p => p.id) }
}
});
console.log('[emptyAllTrash] Deleted', commentCount.count, 'comments');
for (const photo of deletedPhotos) {
if (photo.imageUrl) {
const filePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted display file:', filePath);
}
catch (e) {
console.error('[emptyAllTrash] Error deleting display file:', e);
}
}
}
if (photo.originalUrl) {
const filePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted original file:', filePath);
}
catch (e) {
console.error('[emptyAllTrash] Error deleting original file:', e);
}
}
}
}
console.log('[emptyAllTrash] Deleting all', deletedPhotos.length, 'photos from database');
const photoResult = await this.prisma.photo.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', photoResult.count, 'photos');
deleteResults.success += photoResult.count;
console.log('[emptyAllTrash] Deleting all deleted tours');
const tourResult = await this.prisma.tour.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', tourResult.count, 'tours');
deleteResults.success += tourResult.count;
console.log('[emptyAllTrash] Deleting all deleted notes');
const noteResult = await this.prisma.tourNote.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', noteResult.count, 'notes');
deleteResults.success += noteResult.count;
console.log('[emptyAllTrash] Successfully emptied all trash - total deleted:', deleteResults.success);
}
catch (e) {
console.error('[emptyAllTrash] Error emptying trash:', e?.message || e);
deleteResults.failed++;
deleteResults.errors.push(`Emptying trash: ${e?.message || 'Unknown error'}`);
}
return {
success: true,
totalDeleted: deleteResults.success,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
} }
}; };
__decorate([ __decorate([
@@ -4437,6 +4769,12 @@ __decorate([
__metadata("design:paramtypes", [Object]), __metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise) __metadata("design:returntype", Promise)
], AdminTrashController.prototype, "deletePermanentItems", null); ], AdminTrashController.prototype, "deletePermanentItems", null);
__decorate([
(0, common_1.Post)('empty-all'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], AdminTrashController.prototype, "emptyAllTrash", null);
AdminTrashController = __decorate([ AdminTrashController = __decorate([
(0, common_1.Controller)('admin/trash'), (0, common_1.Controller)('admin/trash'),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
+1 -1
View File
File diff suppressed because one or more lines are too long
+213 -68
View File
@@ -4319,100 +4319,245 @@ class AdminTrashController {
throw new BadRequestException('Tham số không hợp lệ.'); throw new BadRequestException('Tham số không hợp lệ.');
} }
const deleteResults = { success: 0, failed: 0, errors: [] as string[] };
if (type === 'tour') { if (type === 'tour') {
for (const tourId of ids) { for (const tourId of ids) {
console.log('[deletePermanent] Deleting tour:', tourId); try {
const photos = await this.prisma.photo.findMany({ where: { tourId } }); console.log('[deletePermanent] Deleting tour:', tourId);
console.log('[deletePermanent] Found', photos.length, 'photos for tour');
// First, delete all files from disk
for (const p of photos) { const photos = await this.prisma.photo.findMany({ where: { tourId } });
if (p.imageUrl) { console.log('[deletePermanent] Found', photos.length, 'photos for tour');
const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, ''));
for (const p of photos) {
if (p.imageUrl) {
const displayFilePath = path.join(process.cwd(), p.imageUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking display file:', displayFilePath);
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
console.log('[deletePermanent] Deleted display file:', displayFilePath);
} catch (e) {
console.error('[deletePermanent] Error deleting display file:', e);
}
}
}
if (p.originalUrl) {
const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking original file:', originalFilePath);
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
console.log('[deletePermanent] Deleted original file:', originalFilePath);
} catch (e) {
console.error('[deletePermanent] Error deleting original file:', e);
}
}
}
}
// Delete the tour - cascade will handle related records
await this.prisma.tour.delete({ where: { id: tourId } });
console.log('[deletePermanent] Deleted tour from database:', tourId);
deleteResults.success++;
} catch (e: any) {
console.error('[deletePermanent] Error deleting tour:', tourId, e);
deleteResults.failed++;
deleteResults.errors.push(`Tour ${tourId}: ${e?.message || 'Unknown error'}`);
}
}
} else if (type === 'photo') {
console.log('[deletePermanent] Deleting', ids.length, 'photos');
console.log('[deletePermanent] Photo IDs:', JSON.stringify(ids));
if (!ids || ids.length === 0) {
console.warn('[deletePermanent] No photo IDs provided');
return { success: true, deleted: 0, failed: 0 };
}
for (const photoId of ids) {
try {
console.log('[deletePermanent] ========== START Processing photo:', photoId);
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
if (!photo) {
console.warn('[deletePermanent] Photo not found in DB:', photoId);
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: not found in database`);
console.log('[deletePermanent] ========== SKIP (not found):', photoId);
continue;
}
console.log('[deletePermanent] Found photo:', { id: photo.id, imageUrl: photo.imageUrl, isDeleted: photo.isDeleted, tourId: photo.tourId });
// First, delete all comments for this photo (because of foreign key)
console.log('[deletePermanent] Deleting comments for photo:', photoId);
try {
const commentResult = await this.prisma.comment.deleteMany({
where: { photoId }
});
console.log('[deletePermanent] Deleted', commentResult.count, 'comments for photo');
} catch (e: any) {
console.warn('[deletePermanent] Error deleting comments (continuing):', e?.message);
}
// Delete files from disk
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking display file:', displayFilePath); console.log('[deletePermanent] Checking display file:', displayFilePath);
if (fs.existsSync(displayFilePath)) { if (fs.existsSync(displayFilePath)) {
try { try {
fs.unlinkSync(displayFilePath); fs.unlinkSync(displayFilePath);
console.log('[deletePermanent] Deleted display file:', displayFilePath); console.log('[deletePermanent] Deleted display file');
} catch (e) { } catch (e) {
console.error('[deletePermanent] Error deleting display file:', e); console.error('[deletePermanent] Error deleting display file:', e);
} }
} else {
console.log('[deletePermanent] Display file not found (OK):', displayFilePath);
} }
} }
if (p.originalUrl) {
const originalFilePath = path.join(process.cwd(), p.originalUrl.replace(/^\//, '')); if (photo.originalUrl) {
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking original file:', originalFilePath); console.log('[deletePermanent] Checking original file:', originalFilePath);
if (fs.existsSync(originalFilePath)) { if (fs.existsSync(originalFilePath)) {
try { try {
fs.unlinkSync(originalFilePath); fs.unlinkSync(originalFilePath);
console.log('[deletePermanent] Deleted original file:', originalFilePath); console.log('[deletePermanent] Deleted original file');
} catch (e) { } catch (e) {
console.error('[deletePermanent] Error deleting original file:', e); console.error('[deletePermanent] Error deleting original file:', e);
} }
} else {
console.log('[deletePermanent] Original file not found (OK):', originalFilePath);
} }
} }
// Delete from database
console.log('[deletePermanent] Attempting database delete for photo:', photoId);
const result = await this.prisma.photo.delete({ where: { id: photoId } });
console.log('[deletePermanent] ✓ Successfully deleted photo from database:', photoId);
deleteResults.success++;
console.log('[deletePermanent] ========== SUCCESS:', photoId);
} catch (e: any) {
console.error('[deletePermanent] ✗ Error deleting photo:', photoId);
console.error('[deletePermanent] Error details:', e?.message || String(e));
deleteResults.failed++;
deleteResults.errors.push(`Photo ${photoId}: ${e?.message || 'Unknown error'}`);
console.log('[deletePermanent] ========== FAILED:', photoId);
} }
await this.prisma.tour.delete({ where: { id: tourId } });
console.log('[deletePermanent] Deleted tour from database:', tourId);
}
} else if (type === 'photo') {
console.log('[deletePermanent] Deleting', ids.length, 'photos');
for (const photoId of ids) {
console.log('[deletePermanent] Processing photo:', photoId);
const photo = await this.prisma.photo.findUnique({ where: { id: photoId } });
if (!photo) {
console.warn('[deletePermanent] Photo not found:', photoId);
continue;
}
console.log('[deletePermanent] Photo found, imageUrl:', photo.imageUrl);
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking display file:', displayFilePath);
if (fs.existsSync(displayFilePath)) {
try {
fs.unlinkSync(displayFilePath);
console.log('[deletePermanent] Deleted display file');
} catch (e) {
console.error('[deletePermanent] Error deleting display file:', e);
}
} else {
console.log('[deletePermanent] Display file not found (OK)');
}
}
if (photo.originalUrl) {
const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
console.log('[deletePermanent] Checking original file:', originalFilePath);
if (fs.existsSync(originalFilePath)) {
try {
fs.unlinkSync(originalFilePath);
console.log('[deletePermanent] Deleted original file');
} catch (e) {
console.error('[deletePermanent] Error deleting original file:', e);
}
} else {
console.log('[deletePermanent] Original file not found (OK)');
}
}
const result = await this.prisma.photo.delete({ where: { id: photoId } });
console.log('[deletePermanent] Deleted photo from database:', photoId);
} }
console.log('[deletePermanent] Photos deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
} else if (type === 'note') { } else if (type === 'note') {
console.log('[deletePermanent] Deleting', ids.length, 'notes'); console.log('[deletePermanent] Deleting', ids.length, 'notes');
const result = await this.prisma.tourNote.deleteMany({ try {
where: { id: { in: ids } } const result = await this.prisma.tourNote.deleteMany({
}); where: { id: { in: ids } }
console.log('[deletePermanent] Deleted notes from database, count:', result.count); });
console.log('[deletePermanent] Deleted notes from database, count:', result.count);
deleteResults.success = result.count;
} catch (e: any) {
console.error('[deletePermanent] Error deleting notes:', e);
deleteResults.failed = ids.length;
deleteResults.errors.push(`Notes deletion: ${e?.message || 'Unknown error'}`);
}
} }
console.log('[deletePermanent] Deletion complete, type:', type); console.log('[deletePermanent] Deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
return { success: true };
if (deleteResults.failed > 0) {
console.warn('[deletePermanent] Errors:', deleteResults.errors);
}
return {
success: true,
deleted: deleteResults.success,
failed: deleteResults.failed,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
}
@Post('empty-all')
async emptyAllTrash() {
console.log('[emptyAllTrash] Starting to empty all trash');
const deleteResults = { success: 0, failed: 0, errors: [] as string[] };
try {
// Get all deleted photos
console.log('[emptyAllTrash] Finding all deleted photos');
const deletedPhotos = await this.prisma.photo.findMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Found', deletedPhotos.length, 'deleted photos');
// Delete all comments from deleted photos
console.log('[emptyAllTrash] Deleting all comments from deleted photos');
const commentCount = await this.prisma.comment.deleteMany({
where: {
photoId: { in: deletedPhotos.map(p => p.id) }
}
});
console.log('[emptyAllTrash] Deleted', commentCount.count, 'comments');
// Delete files from disk
for (const photo of deletedPhotos) {
if (photo.imageUrl) {
const filePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted display file:', filePath);
} catch (e) {
console.error('[emptyAllTrash] Error deleting display file:', e);
}
}
}
if (photo.originalUrl) {
const filePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, ''));
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
console.log('[emptyAllTrash] Deleted original file:', filePath);
} catch (e) {
console.error('[emptyAllTrash] Error deleting original file:', e);
}
}
}
}
// Delete all photos
console.log('[emptyAllTrash] Deleting all', deletedPhotos.length, 'photos from database');
const photoResult = await this.prisma.photo.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', photoResult.count, 'photos');
deleteResults.success += photoResult.count;
// Delete all deleted tours (cascade will handle related)
console.log('[emptyAllTrash] Deleting all deleted tours');
const tourResult = await this.prisma.tour.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', tourResult.count, 'tours');
deleteResults.success += tourResult.count;
// Delete all deleted notes
console.log('[emptyAllTrash] Deleting all deleted notes');
const noteResult = await this.prisma.tourNote.deleteMany({
where: { isDeleted: true }
});
console.log('[emptyAllTrash] Deleted', noteResult.count, 'notes');
deleteResults.success += noteResult.count;
console.log('[emptyAllTrash] Successfully emptied all trash - total deleted:', deleteResults.success);
} catch (e: any) {
console.error('[emptyAllTrash] Error emptying trash:', e?.message || e);
deleteResults.failed++;
deleteResults.errors.push(`Emptying trash: ${e?.message || 'Unknown error'}`);
}
return {
success: true,
totalDeleted: deleteResults.success,
errors: deleteResults.errors.length > 0 ? deleteResults.errors : undefined
};
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 755 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 887 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 224 KiB

+3 -2
View File
@@ -4,9 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script src="https://accounts.google.com/gsi/client" async defer></script>
<title>Travel Planner</title> <title>Travel Planner</title>
<script type="module" crossorigin src="/assets/index-C3QeRTiI.js"></script> <script type="module" crossorigin src="/assets/index-DtsqetSG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-_0oFafPI.css"> <link rel="stylesheet" crossorigin href="/assets/index-sqDjZKsa.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1
View File
File diff suppressed because one or more lines are too long
+155 -28
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { X, User, Trash2, Shield, ShieldAlert, Lock, Unlock, Loader2, Image as ImageIcon, Map, FileText, CheckCircle, Star, Settings } from 'lucide-react'; import { X, User, Trash2, Shield, ShieldAlert, Lock, Unlock, Loader2, Image as ImageIcon, Map, FileText, CheckCircle, Star, Settings } from 'lucide-react';
import { useConfirm } from '../hooks/useConfirm'; import { useConfirm } from '../hooks/useConfirm';
import { useNotification } from '../hooks/useNotification';
interface UserManagementModalProps { interface UserManagementModalProps {
isOpen: boolean; isOpen: boolean;
@@ -9,6 +10,7 @@ interface UserManagementModalProps {
export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen, onClose }) => { export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen, onClose }) => {
const confirm = useConfirm(); const confirm = useConfirm();
const notify = useNotification();
const [activeTab, setActiveTab] = useState<'users' | 'tours' | 'photos' | 'notes' | 'recommendations' | 'trash' | 'filters' | 'reports'>('users'); const [activeTab, setActiveTab] = useState<'users' | 'tours' | 'photos' | 'notes' | 'recommendations' | 'trash' | 'filters' | 'reports'>('users');
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<any[]>([]);
const [photos, setPhotos] = useState<any[]>([]); const [photos, setPhotos] = useState<any[]>([]);
@@ -116,7 +118,10 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
if (!confirm('Bạn có chắc chắn muốn xóa người dùng này?')) return; if (!await confirm({
title: 'Xóa người dùng',
message: 'Bạn có chắc chắn muốn xóa người dùng này?'
})) return;
try { try {
const res = await fetch(`/api/v1/users/${id}`, { const res = await fetch(`/api/v1/users/${id}`, {
method: 'DELETE', method: 'DELETE',
@@ -469,9 +474,18 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
const handleRestoreTrash = async () => { const handleRestoreTrash = async () => {
if (selectedTrashIds.length === 0) return; if (selectedTrashIds.length === 0) return;
if (!confirm(`Bạn có chắc muốn khôi phục ${selectedTrashIds.length} mục đã chọn?`)) return; if (!await confirm({
title: 'Khôi phục các mục',
message: `Bạn có chắc muốn khôi phục ${selectedTrashIds.length} mục đã chọn?`
})) return;
setTrashLoading(true); setTrashLoading(true);
const idsToRestore = [...selectedTrashIds];
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
try { try {
console.log(`[Trash] Starting restore for ${idsToRestore.length} ${itemType} items`);
const res = await fetch('/api/v1/admin/trash/restore', { const res = await fetch('/api/v1/admin/trash/restore', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -479,31 +493,49 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('token')}`
}, },
body: JSON.stringify({ body: JSON.stringify({
type: trashSubTab, type: itemType,
ids: selectedTrashIds ids: idsToRestore
}) })
}); });
if (res.ok) { if (res.ok) {
console.log('[Trash] Restore successful');
setSelectedTrashIds([]); setSelectedTrashIds([]);
fetchTrashData(); notify({ title: '✓ Thành công', message: `Đã khôi phục ${idsToRestore.length} mục.`, type: 'success' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
} else {
const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Restore failed:', errorMsg);
notify({ title: '✗ Khôi phục thất bại', message: errorMsg, type: 'error' });
setTrashLoading(false);
} }
} catch (e) { } catch (e: any) {
console.error(e); console.error('[Trash] Restore error:', e);
} finally { notify({ title: '✗ Lỗi', message: e?.message || 'Lỗi khi khôi phục các mục', type: 'error' });
setTrashLoading(false); setTrashLoading(false);
} }
}; };
const handleDeletePermanentTrash = async () => { const handleDeletePermanentTrash = async () => {
if (selectedTrashIds.length === 0) return; if (selectedTrashIds.length === 0) return;
if (!confirm(`CẢNH BÁO: Bạn có chắc muốn xóa VĨNH VIỄN ${selectedTrashIds.length} mục đã chọn? Thao tác này không thể hoàn tác.`)) return; if (!await confirm({
title: '⚠️ XÓA VĨNH VIỄN',
message: `Cảnh báo: Bạn có chắc muốn XÓA VĨNH VIỄN ${selectedTrashIds.length} mục đã chọn? Thao tác này KHÔNG THỂ hoàn tác!`
})) return;
setTrashLoading(true); setTrashLoading(true);
const idsToDelete = [...selectedTrashIds]; const idsToDelete = [...selectedTrashIds];
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
setSelectedTrashIds([]); setSelectedTrashIds([]);
try { try {
console.log('[Trash] Starting permanent delete for', idsToDelete.length, 'items'); console.log(`[Trash] Starting permanent delete for ${idsToDelete.length} ${itemType} items:`, idsToDelete);
const res = await fetch('/api/v1/admin/trash/delete-permanent', { const res = await fetch('/api/v1/admin/trash/delete-permanent', {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -511,7 +543,7 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('token')}`
}, },
body: JSON.stringify({ body: JSON.stringify({
type: trashSubTab, type: itemType,
ids: idsToDelete ids: idsToDelete
}) })
}); });
@@ -519,36 +551,117 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
console.log('[Trash] Delete response status:', res.status); console.log('[Trash] Delete response status:', res.status);
if (res.ok) { if (res.ok) {
console.log('[Trash] Delete successful, clearing state immediately'); const responseData = await res.json();
console.log('[Trash] Delete successful, response:', responseData);
setError(''); setError('');
alert('Đã xóa vĩnh viễn thành công!');
// Small delay to let alert close, then refresh data // Build success message
const { deleted = 0, failed = 0, errors = [] } = responseData;
if (failed === 0) {
notify({ title: '✓ Xóa thành công', message: `Đã xóa vĩnh viễn ${deleted} mục.`, type: 'success' });
} else {
let errorMsg = `Xóa thành công ${deleted} mục, thất bại ${failed} mục.`;
if (errors && errors.length > 0) {
errorMsg += ` Lỗi: ${errors[0]}`;
}
notify({ title: '⚠️ Xóa một phần', message: errorMsg, type: 'error' });
}
// Refresh trash data after delete
setTimeout(() => { setTimeout(() => {
console.log('[Trash] Refreshing trash data after delete'); console.log('[Trash] Refreshing trash data after delete');
fetchTrashData(); fetchTrashData();
setTrashLoading(false); setTrashLoading(false);
}, 500); }, 800);
return; // Important: exit early so finally doesn't run again
} else { } else {
const errorData = await res.json().catch(() => ({})); const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData.message || `HTTP ${res.status}`; const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Delete failed:', errorMsg); console.error('[Trash] Delete failed with status', res.status, ':', errorMsg);
setError(errorMsg); setError(errorMsg);
alert(`Xóa thất bại: ${errorMsg}`); notify({ title: '✗ Xóa thất bại', message: errorMsg, type: 'error' });
// Refresh on error // Refresh to see current state
fetchTrashData(); setTimeout(() => {
setTrashLoading(false); fetchTrashData();
setTrashLoading(false);
}, 500);
} }
} catch (e: any) { } catch (e: any) {
console.error('[Trash] Delete error:', e); console.error('[Trash] Delete error:', e);
setError(e.message || 'Lỗi khi xóa các mục'); const errorMsg = e?.message || 'Lỗi mạng khi xóa các mục';
alert(`Lỗi: ${e.message}`); setError(errorMsg);
notify({ title: '✗ Lỗi', message: errorMsg, type: 'error' });
// Refresh on error // Refresh on error
fetchTrashData(); setTimeout(() => {
setTrashLoading(false); fetchTrashData();
setTrashLoading(false);
}, 500);
}
};
const handleEmptyAllTrash = async () => {
if (!await confirm({
title: '⚠️ LÀMTRỐNG THÙNG RÁC',
message: `Cảnh báo: Thao tác này sẽ XÓA VĨNH VIỄN TẤT CẢ mục trong thùng rác! KHÔNG THỂ hoàn tác!`
})) return;
setTrashLoading(true);
try {
console.log('[Trash] Starting to empty ALL trash');
const res = await fetch('/api/v1/admin/trash/empty-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
console.log('[Trash] Empty all response status:', res.status);
if (res.ok) {
const responseData = await res.json();
console.log('[Trash] Empty all successful, response:', responseData);
setError('');
setSelectedTrashIds([]);
const { totalDeleted = 0 } = responseData;
notify({
title: '✓ Thùng rác đã được làm trống',
message: `Đã xóa vĩnh viễn ${totalDeleted} mục.`,
type: 'success'
});
// Refresh trash data
setTimeout(() => {
console.log('[Trash] Refreshing trash data after empty all');
fetchTrashData();
setTrashLoading(false);
}, 800);
} else {
const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Empty all failed with status', res.status, ':', errorMsg);
setError(errorMsg);
notify({ title: '✗ Làm trống thất bại', message: errorMsg, type: 'error' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
}
} catch (e: any) {
console.error('[Trash] Empty all error:', e);
const errorMsg = e?.message || 'Lỗi mạng khi làm trống thùng rác';
setError(errorMsg);
notify({ title: '✗ Lỗi', message: errorMsg, type: 'error' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
} }
}; };
@@ -563,11 +676,11 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
body: JSON.stringify({ days: retentionDaysInput }) body: JSON.stringify({ days: retentionDaysInput })
}); });
if (res.ok) { if (res.ok) {
alert('Đã cập nhật số ngày lưu trữ trong thùng rác.'); notify({ title: '✓ Cập nhật thành công', message: 'Số ngày lưu trữ đã được cập nhật.', type: 'success' });
fetchTrashData(); fetchTrashData();
} else { } else {
const data = await res.json(); const data = await res.json();
alert(data.message || 'Lỗi khi cập nhật số ngày lưu trữ.'); notify({ title: '✗ Lỗi', message: data.message || 'Lỗi khi cập nhật số ngày lưu trữ.', type: 'error' });
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -1263,6 +1376,20 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
</div> </div>
</div> </div>
{/* Empty All Trash button */}
{(trashData.tours?.length > 0 || trashData.photos?.length > 0 || trashData.notes?.length > 0) && (
<div className="bg-red-50 p-4 rounded-2xl border border-red-100">
<button
onClick={handleEmptyAllTrash}
disabled={trashLoading}
className="w-full px-4 py-3 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white rounded-xl font-bold text-sm uppercase tracking-wide flex items-center justify-center gap-2 shadow-md transition-all active:scale-95 cursor-pointer"
>
{trashLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
Làm trống thùng rác ({(trashData.tours?.length || 0) + (trashData.photos?.length || 0) + (trashData.notes?.length || 0)} mục)
</button>
</div>
)}
{/* Sub-tab selection */} {/* Sub-tab selection */}
<div className="flex gap-2 border-b border-gray-100 pb-2"> <div className="flex gap-2 border-b border-gray-100 pb-2">
<button <button