fix: admin empty thùng rác
@@ -1,4 +1,8 @@
|
||||
declare const JwtAuthGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
|
||||
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 {};
|
||||
|
||||
@@ -6,13 +6,29 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.JwtAuthGuard = void 0;
|
||||
exports.JwtAuthGuardNoAnonymous = exports.JwtAuthGuard = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const passport_1 = require("@nestjs/passport");
|
||||
const common_2 = require("@nestjs/common");
|
||||
let JwtAuthGuard = class JwtAuthGuard extends (0, passport_1.AuthGuard)('jwt') {
|
||||
};
|
||||
exports.JwtAuthGuard = JwtAuthGuard;
|
||||
exports.JwtAuthGuard = JwtAuthGuard = __decorate([
|
||||
(0, common_1.Injectable)()
|
||||
], 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
|
||||
@@ -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 @@
|
||||
{"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"}
|
||||
@@ -71,6 +71,7 @@ const admin_guard_1 = require("./auth/admin.guard");
|
||||
const nodemailer = __importStar(require("nodemailer"));
|
||||
const jwt_1 = require("@nestjs/jwt");
|
||||
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 core_2 = require("@nestjs/core");
|
||||
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`);
|
||||
return tour;
|
||||
}
|
||||
@@ -1464,6 +1493,18 @@ let TourController = class TourController {
|
||||
console.warn('[HEIC] Failed to convert HEIC to JPEG, attempting to process anyway:', e.message);
|
||||
}
|
||||
}
|
||||
const uploadsDir = path.dirname(displayFilePath);
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
try {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
console.log(`[Photo] Created directory: ${uploadsDir}`);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(`[Photo] Failed to create directory ${uploadsDir}:`, e);
|
||||
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, {
|
||||
@@ -1472,7 +1513,17 @@ let TourController = class TourController {
|
||||
})
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(displayFilePath);
|
||||
return this.prisma.photo.create({
|
||||
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: {
|
||||
tourId: tourId,
|
||||
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) {
|
||||
@@ -1528,17 +1581,31 @@ let TourController = class TourController {
|
||||
}
|
||||
async joinByToken(body, req) {
|
||||
const { token } = body;
|
||||
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
|
||||
if (!token) {
|
||||
console.error('[joinByToken] No token provided');
|
||||
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({
|
||||
where: { token },
|
||||
include: { tour: { select: { title: true } } }
|
||||
include: { tour: { select: { id: true, title: true } } }
|
||||
});
|
||||
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.');
|
||||
}
|
||||
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()) {
|
||||
console.error('[joinByToken] Invitation expired at:', invitation.expiredAt);
|
||||
try {
|
||||
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.');
|
||||
}
|
||||
const userId = req.user.id;
|
||||
console.log('[joinByToken] User attempting to join - userId:', userId, 'tourId:', invitation.tourId);
|
||||
const existingParticipation = await this.prisma.tourParticipant.findUnique({
|
||||
where: { tourId_userId: { tourId: invitation.tourId, userId } }
|
||||
});
|
||||
if (existingParticipation) {
|
||||
console.log('[joinByToken] User is already a participant');
|
||||
try {
|
||||
await this.prisma.tourInvitation.delete({ where: { id: invitation.id } });
|
||||
}
|
||||
@@ -1582,6 +1651,7 @@ let TourController = class TourController {
|
||||
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 };
|
||||
}
|
||||
async mergeMember(tourId, body, req) {
|
||||
@@ -1709,7 +1779,7 @@ __decorate([
|
||||
], TourController.prototype, "deleteTour", null);
|
||||
__decorate([
|
||||
(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'),
|
||||
__param(0, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
@@ -2155,11 +2225,15 @@ let PhotoController = class PhotoController {
|
||||
const originalFilename = `${uniqueSuffix}${originalExtension}`;
|
||||
const displayFilename = `${uniqueSuffix}.jpg`;
|
||||
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);
|
||||
if (!fs.existsSync(memberOriginalDir)) {
|
||||
fs.mkdirSync(memberOriginalDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(tourDisplayPath)) {
|
||||
fs.mkdirSync(tourDisplayPath, { recursive: true });
|
||||
}
|
||||
await fs.promises.writeFile(originalFilePath, file.buffer);
|
||||
let lat;
|
||||
let lng;
|
||||
@@ -3037,6 +3111,23 @@ let PublicPhotoController = class PublicPhotoController {
|
||||
</html>`;
|
||||
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([
|
||||
(0, common_1.Get)(),
|
||||
@@ -3079,6 +3170,16 @@ __decorate([
|
||||
__metadata("design:paramtypes", [String, Object, Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], 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([
|
||||
(0, common_1.Controller)('public-photos'),
|
||||
__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),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
], 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 {
|
||||
constructor(prisma) {
|
||||
this.prisma = prisma;
|
||||
@@ -4347,67 +4521,225 @@ let AdminTrashController = class AdminTrashController {
|
||||
}
|
||||
async deletePermanentItems(body) {
|
||||
const { type, ids } = body;
|
||||
console.log('[deletePermanent] Request received - type:', type, 'ids count:', ids.length);
|
||||
if (!type || !ids || !Array.isArray(ids)) {
|
||||
throw new common_1.BadRequestException('Tham số không hợp lệ.');
|
||||
}
|
||||
const deleteResults = { success: 0, failed: 0, errors: [] };
|
||||
if (type === 'tour') {
|
||||
for (const tourId of ids) {
|
||||
try {
|
||||
console.log('[deletePermanent] Deleting tour:', tourId);
|
||||
const photos = await this.prisma.photo.findMany({ where: { tourId } });
|
||||
console.log('[deletePermanent] Found', photos.length, 'photos for tour');
|
||||
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);
|
||||
}
|
||||
catch (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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
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'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
}
|
||||
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) { }
|
||||
catch (e) {
|
||||
console.error('[deletePermanent] Error deleting original file:', 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') {
|
||||
await this.prisma.tourNote.deleteMany({
|
||||
console.log('[deletePermanent] Deleting', ids.length, 'notes');
|
||||
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;
|
||||
}
|
||||
return { success: true };
|
||||
catch (e) {
|
||||
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 - 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([
|
||||
@@ -4437,6 +4769,12 @@ __decorate([
|
||||
__metadata("design:paramtypes", [Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], 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([
|
||||
(0, common_1.Controller)('admin/trash'),
|
||||
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard),
|
||||
|
||||
@@ -4319,9 +4319,14 @@ class AdminTrashController {
|
||||
throw new BadRequestException('Tham số không hợp lệ.');
|
||||
}
|
||||
|
||||
const deleteResults = { success: 0, failed: 0, errors: [] as string[] };
|
||||
|
||||
if (type === 'tour') {
|
||||
for (const tourId of ids) {
|
||||
try {
|
||||
console.log('[deletePermanent] Deleting tour:', tourId);
|
||||
|
||||
// First, delete all files from disk
|
||||
const photos = await this.prisma.photo.findMany({ where: { tourId } });
|
||||
console.log('[deletePermanent] Found', photos.length, 'photos for tour');
|
||||
|
||||
@@ -4336,8 +4341,6 @@ class AdminTrashController {
|
||||
} catch (e) {
|
||||
console.error('[deletePermanent] Error deleting display file:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('[deletePermanent] Display file not found (OK):', displayFilePath);
|
||||
}
|
||||
}
|
||||
if (p.originalUrl) {
|
||||
@@ -4350,38 +4353,66 @@ class AdminTrashController {
|
||||
} catch (e) {
|
||||
console.error('[deletePermanent] Error deleting original file:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('[deletePermanent] Original file not found (OK):', originalFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.log('[deletePermanent] Processing photo:', photoId);
|
||||
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:', photoId);
|
||||
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] Photo found, imageUrl:', photo.imageUrl);
|
||||
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);
|
||||
if (fs.existsSync(displayFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(displayFilePath);
|
||||
console.log('[deletePermanent] Deleted display file');
|
||||
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)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4391,28 +4422,142 @@ class AdminTrashController {
|
||||
if (fs.existsSync(originalFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(originalFilePath);
|
||||
console.log('[deletePermanent] Deleted original file');
|
||||
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)');
|
||||
}
|
||||
}
|
||||
|
||||
// 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] Deleted photo from database:', 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);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[deletePermanent] Photos deletion complete - success:', deleteResults.success, 'failed:', deleteResults.failed);
|
||||
} else if (type === 'note') {
|
||||
console.log('[deletePermanent] Deleting', ids.length, 'notes');
|
||||
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: 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);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 756 KiB |
|
Before Width: | Height: | Size: 1003 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 808 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 889 KiB |
|
Before Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 994 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 806 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 516 KiB |
|
Before Width: | Height: | Size: 755 KiB |
|
Before Width: | Height: | Size: 887 KiB |
|
Before Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 681 KiB |
|
Before Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 224 KiB |
@@ -4,9 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<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>
|
||||
<script type="module" crossorigin src="/assets/index-C3QeRTiI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-_0oFafPI.css">
|
||||
<script type="module" crossorigin src="/assets/index-DtsqetSG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-sqDjZKsa.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { useConfirm } from '../hooks/useConfirm';
|
||||
import { useNotification } from '../hooks/useNotification';
|
||||
|
||||
interface UserManagementModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -9,6 +10,7 @@ interface UserManagementModalProps {
|
||||
|
||||
export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen, onClose }) => {
|
||||
const confirm = useConfirm();
|
||||
const notify = useNotification();
|
||||
const [activeTab, setActiveTab] = useState<'users' | 'tours' | 'photos' | 'notes' | 'recommendations' | 'trash' | 'filters' | 'reports'>('users');
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [photos, setPhotos] = useState<any[]>([]);
|
||||
@@ -116,7 +118,10 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
};
|
||||
|
||||
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 {
|
||||
const res = await fetch(`/api/v1/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -469,9 +474,18 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
|
||||
const handleRestoreTrash = async () => {
|
||||
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);
|
||||
const idsToRestore = [...selectedTrashIds];
|
||||
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
|
||||
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
|
||||
|
||||
try {
|
||||
console.log(`[Trash] Starting restore for ${idsToRestore.length} ${itemType} items`);
|
||||
const res = await fetch('/api/v1/admin/trash/restore', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -479,31 +493,49 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: trashSubTab,
|
||||
ids: selectedTrashIds
|
||||
type: itemType,
|
||||
ids: idsToRestore
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
console.log('[Trash] Restore successful');
|
||||
setSelectedTrashIds([]);
|
||||
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) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
} catch (e: any) {
|
||||
console.error('[Trash] Restore error:', e);
|
||||
notify({ title: '✗ Lỗi', message: e?.message || 'Lỗi khi khôi phục các mục', type: 'error' });
|
||||
setTrashLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePermanentTrash = async () => {
|
||||
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);
|
||||
const idsToDelete = [...selectedTrashIds];
|
||||
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
|
||||
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
|
||||
setSelectedTrashIds([]);
|
||||
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -511,7 +543,7 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: trashSubTab,
|
||||
type: itemType,
|
||||
ids: idsToDelete
|
||||
})
|
||||
});
|
||||
@@ -519,36 +551,117 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
console.log('[Trash] Delete response status:', res.status);
|
||||
|
||||
if (res.ok) {
|
||||
console.log('[Trash] Delete successful, clearing state immediately');
|
||||
const responseData = await res.json();
|
||||
console.log('[Trash] Delete successful, response:', responseData);
|
||||
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(() => {
|
||||
console.log('[Trash] Refreshing trash data after delete');
|
||||
fetchTrashData();
|
||||
setTrashLoading(false);
|
||||
}, 500);
|
||||
return; // Important: exit early so finally doesn't run again
|
||||
}, 800);
|
||||
} else {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
const errorMsg = errorData.message || `HTTP ${res.status}`;
|
||||
console.error('[Trash] Delete failed:', errorMsg);
|
||||
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
|
||||
console.error('[Trash] Delete failed with status', res.status, ':', 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
|
||||
setTimeout(() => {
|
||||
fetchTrashData();
|
||||
setTrashLoading(false);
|
||||
}, 500);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[Trash] Delete error:', e);
|
||||
setError(e.message || 'Lỗi khi xóa các mục');
|
||||
alert(`Lỗi: ${e.message}`);
|
||||
const errorMsg = e?.message || 'Lỗi mạng khi xóa các mục';
|
||||
setError(errorMsg);
|
||||
notify({ title: '✗ Lỗi', message: errorMsg, type: 'error' });
|
||||
|
||||
// Refresh on error
|
||||
setTimeout(() => {
|
||||
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 })
|
||||
});
|
||||
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();
|
||||
} else {
|
||||
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) {
|
||||
console.error(e);
|
||||
@@ -1263,6 +1376,20 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
|
||||
</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 */}
|
||||
<div className="flex gap-2 border-b border-gray-100 pb-2">
|
||||
<button
|
||||
|
||||