diff --git a/server/prisma/migrations/20251116222516_add_cleaned_fields/migration.sql b/server/prisma/migrations/20251116222516_add_cleaned_fields/migration.sql new file mode 100644 index 0000000..4a11589 --- /dev/null +++ b/server/prisma/migrations/20251116222516_add_cleaned_fields/migration.sql @@ -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; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index c1d364a..003733d 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -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") } diff --git a/server/src/controllers/reportController.ts b/server/src/controllers/reportController.ts index cbb70f2..b922fff 100644 --- a/server/src/controllers/reportController.ts +++ b/server/src/controllers/reportController.ts @@ -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(); diff --git a/server/src/routes/reportRoutes.ts b/server/src/routes/reportRoutes.ts index 03ad752..7a20411 100644 --- a/server/src/routes/reportRoutes.ts +++ b/server/src/routes/reportRoutes.ts @@ -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; diff --git a/server/src/services/reportService.ts b/server/src/services/reportService.ts index 7f2f35f..e8583d0 100644 --- a/server/src/services/reportService.ts +++ b/server/src/services/reportService.ts @@ -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(); diff --git a/src/components/report/ReportDetailModal.tsx b/src/components/report/ReportDetailModal.tsx index 583ef43..448b2c2 100644 --- a/src/components/report/ReportDetailModal.tsx +++ b/src/components/report/ReportDetailModal.tsx @@ -9,9 +9,10 @@ interface ReportDetailModalProps { onClose: () => void; onVerify?: (reportId: string, comment?: string) => void; onDispute?: (reportId: string, comment?: string) => void; + onMarkCleaned?: (reportId: string) => void; } -export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute }: ReportDetailModalProps) { +export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute, onMarkCleaned }: ReportDetailModalProps) { const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const [showVerifyComment, setShowVerifyComment] = useState(false); const [showDisputeComment, setShowDisputeComment] = useState(false); @@ -357,35 +358,55 @@ export function ReportDetailModal({ report, isOpen, onClose, onVerify, onDispute {/* Action Buttons */} - {(onVerify || onDispute) && ( + {(onVerify || onDispute || onMarkCleaned) && (
- Help maintain data quality by verifying or disputing this report. -
+ {(onVerify || onDispute) && ( ++ Help maintain data quality by verifying or disputing this report. +
+ )} {!showVerifyComment && !showDisputeComment && ( -