Files
ocean-cleanup/server/src/services/reportService.ts
Paul Bailey 64fe312f01 feat: implement report submission with photo upload and map display
- Fix Prisma client generation and database schema sync issues
- Implement user auto-creation from Clerk authentication
- Add raw SQL queries to handle MySQL POINT spatial fields
- Make password_hash optional for Clerk-only authentication
- Update MapPage to fetch real reports from API
- Add photo display in map popups with full-size modal view
- Configure CORS and Helmet for cross-origin image loading
- Add cleanup tracking task to implementation plan

Fixes report submission 500 errors and enables end-to-end workflow from submission to map display.
2025-11-16 20:01:26 +00:00

185 lines
4.6 KiB
TypeScript

import prisma from '../config/database';
import { Prisma } from '@prisma/client';
export interface CreateReportData {
userId: string;
latitude: number;
longitude: number;
locationSource: 'exif' | 'gps' | 'manual';
photoUrls: string[];
photoTimestamp?: Date;
litterType: string;
quantity: string;
description?: string;
environmentalConcerns?: Array<{
type: string;
severity: string;
description: string;
}>;
}
export class ReportService {
async createReport(data: CreateReportData) {
// Create report using raw SQL to handle the POINT field
const reportId = await prisma.$queryRaw<Array<{ id: string }>>`
INSERT INTO reports (
user_id, location, latitude, longitude, location_source,
photo_urls, photo_timestamp, litter_type, quantity, description
) VALUES (
${data.userId},
ST_GeomFromText(${`POINT(${data.longitude} ${data.latitude})`}, 4326),
${data.latitude},
${data.longitude},
${data.locationSource},
${JSON.stringify(data.photoUrls)},
${data.photoTimestamp || null},
${data.litterType},
${data.quantity},
${data.description || null}
)
`;
// Get the created report ID
const createdId = await prisma.$queryRaw<Array<{ id: string }>>`
SELECT id FROM reports ORDER BY created_at DESC LIMIT 1
`;
// Create environmental concerns if provided
if (data.environmentalConcerns && data.environmentalConcerns.length > 0) {
for (const concern of data.environmentalConcerns) {
await prisma.$queryRaw`
INSERT INTO environmental_concerns (report_id, concern_type, severity, description)
VALUES (${createdId[0].id}, ${concern.type}, ${concern.severity}, ${concern.description})
`;
}
}
// Fetch and return the complete report
const report = await prisma.report.findUnique({
where: { id: createdId[0].id },
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
},
});
return report;
}
async getReports(filters?: {
startDate?: Date;
endDate?: Date;
litterTypes?: string[];
quantities?: string[];
verificationStatus?: string[];
bounds?: { north: number; south: number; east: number; west: number };
}) {
const where: any = {};
if (filters?.startDate || filters?.endDate) {
where.createdAt = {};
if (filters.startDate) where.createdAt.gte = filters.startDate;
if (filters.endDate) where.createdAt.lte = filters.endDate;
}
if (filters?.litterTypes?.length) {
where.litterType = { in: filters.litterTypes };
}
if (filters?.quantities?.length) {
where.quantity = { in: filters.quantities };
}
if (filters?.verificationStatus?.length) {
where.verificationStatus = { in: filters.verificationStatus };
}
if (filters?.bounds) {
where.latitude = {
gte: new Prisma.Decimal(filters.bounds.south),
lte: new Prisma.Decimal(filters.bounds.north),
};
where.longitude = {
gte: new Prisma.Decimal(filters.bounds.west),
lte: new Prisma.Decimal(filters.bounds.east),
};
}
const reports = await prisma.report.findMany({
where,
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
verifications: true,
},
orderBy: {
createdAt: 'desc',
},
});
return reports;
}
async getReportById(id: string) {
const report = await prisma.report.findUnique({
where: { id },
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
verifications: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
},
});
return report;
}
async deleteReport(id: string, userId: string) {
// Verify ownership
const report = await prisma.report.findUnique({
where: { id },
select: { userId: true },
});
if (!report) {
throw new Error('Report not found');
}
if (report.userId !== userId) {
throw new Error('Unauthorized to delete this report');
}
await prisma.report.delete({
where: { id },
});
}
}
export const reportService = new ReportService();