- 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.
185 lines
4.6 KiB
TypeScript
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();
|