Implement report verification interface with audit log

- Add verify/dispute endpoints to backend API
- Create verification service methods with auto-hide logic for 3+ disputes
- Update ReportDetailModal with verification buttons and comment forms
- Add verification audit log display showing history of verifications/disputes
- Implement auto-hide warning for disputed reports
- Update MapPage to handle verification actions and refresh data
- Add verification counts to report responses
- Filter out disputed reports from public view by default
- Add verification test script

Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
This commit is contained in:
2025-11-16 22:11:40 +00:00
parent 53cc84e832
commit f603ea8fda
8 changed files with 567 additions and 38 deletions

View File

@@ -79,6 +79,7 @@ export class ReportService {
quantities?: string[];
verificationStatus?: string[];
bounds?: { north: number; south: number; east: number; west: number };
includeDisputed?: boolean; // Admin flag to include disputed reports
}) {
const where: any = {};
@@ -98,6 +99,9 @@ export class ReportService {
if (filters?.verificationStatus?.length) {
where.verificationStatus = { in: filters.verificationStatus };
} else if (!filters?.includeDisputed) {
// By default, exclude disputed reports (auto-hidden)
where.verificationStatus = { not: 'disputed' };
}
if (filters?.bounds) {
@@ -122,14 +126,33 @@ export class ReportService {
},
},
environmentalConcerns: true,
verifications: true,
verifications: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
});
return reports;
// Add verification and dispute counts to each report
return reports.map(report => {
const verificationCount = report.verifications.filter(v => v.verificationType === 'verify').length;
const disputeCount = report.verifications.filter(v => v.verificationType === 'dispute').length;
return {
...report,
verificationCount,
disputeCount,
};
});
}
async getReportById(id: string) {
@@ -153,11 +176,26 @@ export class ReportService {
},
},
},
orderBy: {
createdAt: 'desc',
},
},
},
});
return report;
if (!report) {
return null;
}
// Calculate verification and dispute counts
const verificationCount = report.verifications.filter(v => v.verificationType === 'verify').length;
const disputeCount = report.verifications.filter(v => v.verificationType === 'dispute').length;
return {
...report,
verificationCount,
disputeCount,
};
}
async deleteReport(id: string, userId: string) {
@@ -179,6 +217,115 @@ export class ReportService {
where: { id },
});
}
async verifyReport(reportId: string, userId: string, comment?: string) {
// Check if report exists
const report = await prisma.report.findUnique({
where: { id: reportId },
});
if (!report) {
throw new Error('Report not found');
}
// Check if user already verified/disputed this report
const existingVerification = await prisma.verification.findUnique({
where: {
reportId_userId: {
reportId,
userId,
},
},
});
if (existingVerification) {
throw new Error('You have already verified or disputed this report');
}
// Create verification
await prisma.verification.create({
data: {
reportId,
userId,
verificationType: 'verify',
comment,
},
});
// Update verification counts and status
await this.updateReportVerificationStatus(reportId);
// Return updated report
return this.getReportById(reportId);
}
async disputeReport(reportId: string, userId: string, comment?: string) {
// Check if report exists
const report = await prisma.report.findUnique({
where: { id: reportId },
});
if (!report) {
throw new Error('Report not found');
}
// Check if user already verified/disputed this report
const existingVerification = await prisma.verification.findUnique({
where: {
reportId_userId: {
reportId,
userId,
},
},
});
if (existingVerification) {
throw new Error('You have already verified or disputed this report');
}
// Create dispute
await prisma.verification.create({
data: {
reportId,
userId,
verificationType: 'dispute',
comment,
},
});
// Update verification counts and status
await this.updateReportVerificationStatus(reportId);
// Return updated report
return this.getReportById(reportId);
}
private async updateReportVerificationStatus(reportId: string) {
// Count verifications and disputes
const verifications = await prisma.verification.findMany({
where: { reportId },
});
const verifyCount = verifications.filter(v => v.verificationType === 'verify').length;
const disputeCount = verifications.filter(v => v.verificationType === 'dispute').length;
// Determine status based on counts
let status: 'pending' | 'verified' | 'disputed' = 'pending';
if (disputeCount >= 3) {
// Auto-hide: 3+ disputes
status = 'disputed';
} else if (verifyCount >= 2) {
// Verified: 2+ verifications
status = 'verified';
}
// Update report status
await prisma.report.update({
where: { id: reportId },
data: { verificationStatus: status },
});
}
}
export const reportService = new ReportService();