Add 'Mark as Cleaned Up' functionality

- Add cleaned_at and cleaned_by_user_id fields to reports table
- Create database migration for cleanup tracking
- Add markAsCleaned backend endpoint and service method
- Add Mark as Cleaned Up button to ReportDetailModal
- Update types to include cleaned fields
- Add frontend handler to mark reports as cleaned
This commit is contained in:
2025-11-16 22:26:59 +00:00
parent beb5d8e7d8
commit 17e3e85781
9 changed files with 168 additions and 21 deletions

View File

@@ -0,0 +1,10 @@
-- Add cleaned_at and cleaned_by_user_id fields to reports table
ALTER TABLE `reports`
ADD COLUMN `cleaned_at` TIMESTAMP NULL,
ADD COLUMN `cleaned_by_user_id` VARCHAR(36) NULL,
ADD INDEX `idx_cleaned_at` (`cleaned_at`),
ADD CONSTRAINT `reports_cleaned_by_fk`
FOREIGN KEY (`cleaned_by_user_id`)
REFERENCES `users`(`id`)
ON DELETE SET NULL
ON UPDATE NO ACTION;

View File

@@ -25,6 +25,7 @@ model User {
eventRegistrations EventRegistration[]
organizedEvents Event[] @relation("EventOrganizer")
reports Report[]
cleanedReports Report[] @relation("CleanedByUser")
verifications Verification[]
@@index([clerkId], map: "idx_clerk_id")
@@ -49,15 +50,20 @@ model Report {
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0)
submittedAt DateTime? @default(now()) @map("submitted_at") @db.Timestamp(0)
verificationStatus reports_verification_status? @default(pending) @map("verification_status")
cleanedAt DateTime? @map("cleaned_at") @db.Timestamp(0)
cleanedByUserId String? @map("cleaned_by_user_id") @db.VarChar(36)
environmentalConcerns EnvironmentalConcern[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "reports_ibfk_1")
cleanedByUser User? @relation("CleanedByUser", fields: [cleanedByUserId], references: [id], onDelete: SetNull, onUpdate: NoAction, map: "reports_cleaned_by_fk")
verifications Verification[]
@@index([createdAt], map: "idx_created_at")
@@index([cleanedAt], map: "idx_cleaned_at")
@@index([latitude, longitude], map: "idx_lat_lng")
@@index([litterType], map: "idx_litter_type")
@@index([location(length: 32)], map: "idx_location")
@@index([userId], map: "user_id")
@@index([cleanedByUserId], map: "cleaned_by_user_id")
@@map("reports")
}

View File

@@ -385,6 +385,49 @@ export class ReportController {
});
}
}
async markAsCleaned(req: AuthRequest, res: Response) {
try {
if (!req.userId) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'User not authenticated',
timestamp: new Date().toISOString(),
},
});
}
const { id } = req.params;
// Find or create user in database
const dbUserId = await this.findOrCreateUser(req.userId);
const report = await reportService.markAsCleaned(id, dbUserId);
res.json(report);
} catch (error: any) {
console.error('Error marking report as cleaned:', error);
if (error.message === 'Report not found') {
return res.status(404).json({
error: {
code: 'RESOURCE_NOT_FOUND',
message: 'Report not found',
timestamp: new Date().toISOString(),
},
});
}
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to mark report as cleaned',
timestamp: new Date().toISOString(),
},
});
}
}
}
export const reportController = new ReportController();

View File

@@ -18,4 +18,7 @@ router.delete('/:id', requireAuth, reportController.deleteReport.bind(reportCont
router.post('/:id/verify', requireAuth, reportController.verifyReport.bind(reportController));
router.post('/:id/dispute', requireAuth, reportController.disputeReport.bind(reportController));
// Cleanup tracking
router.post('/:id/mark-cleaned', requireAuth, reportController.markAsCleaned.bind(reportController));
export default router;

View File

@@ -330,6 +330,29 @@ export class ReportService {
data: { verificationStatus: status },
});
}
async markAsCleaned(reportId: string, userId: string) {
// Check if report exists
const report = await prisma.report.findUnique({
where: { id: reportId },
});
if (!report) {
throw new Error('Report not found');
}
// Mark as cleaned
await prisma.report.update({
where: { id: reportId },
data: {
cleanedAt: new Date(),
cleanedByUserId: userId,
},
});
// Return updated report
return this.getReportById(reportId);
}
}
export const reportService = new ReportService();