feat: implement core platform features including business directory, admin dashboard, authentication, and API infrastructure
Some checks failed
Build and Push App / build (push) Failing after 16m2s
Some checks failed
Build and Push App / build (push) Failing after 16m2s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/admin/reports — List all reports for moderation
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
@@ -37,14 +37,57 @@ export async function GET(
|
||||
// 3. Get monthly stats (views per day for last 30 days)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
||||
|
||||
// Note: Raw query might be needed for group by date in some DBs,
|
||||
// but for now we'll just return the totals for simplicity.
|
||||
const recentEvents = await prisma.analyticsEvent.findMany({
|
||||
where: {
|
||||
createdAt: { gte: thirtyDaysAgo },
|
||||
metadata: {
|
||||
path: ['businessId'],
|
||||
equals: id
|
||||
}
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
createdAt: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Process events into daily stats
|
||||
const statsMap = new Map<string, { date: string, views: number, clicks: number }>();
|
||||
|
||||
// Initialize last 30 days with 0
|
||||
for (let i = 0; i <= 30; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const displayDate = date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
|
||||
statsMap.set(dateStr, { date: displayDate, views: 0, clicks: 0 });
|
||||
}
|
||||
|
||||
recentEvents.forEach(event => {
|
||||
const dateStr = event.createdAt.toISOString().split('T')[0];
|
||||
if (statsMap.has(dateStr)) {
|
||||
const dayStat = statsMap.get(dateStr)!;
|
||||
if (event.type === 'BUSINESS_VIEW') {
|
||||
dayStat.views++;
|
||||
} else if (event.type === 'BUSINESS_CONTACT_CLICK') {
|
||||
dayStat.clicks++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Map to sorted array
|
||||
const dailyStats = Array.from(statsMap.values()).reverse(); // Older to newer
|
||||
|
||||
return NextResponse.json({
|
||||
businessId: id,
|
||||
totalViews,
|
||||
contactClicks,
|
||||
dailyStats,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../lib/prisma';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { getGeoInfo } from '@/lib/geo';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
console.log('>>> ANALYTICS API HIT');
|
||||
try {
|
||||
const body = await request.json();
|
||||
console.log('>>> PAYLOAD:', JSON.stringify(body));
|
||||
const { type, path, label, value, metadata } = body;
|
||||
|
||||
if (!type || !path) {
|
||||
return NextResponse.json({ error: 'Type et Path requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract detailed info from headers
|
||||
const userAgent = request.headers.get('user-agent') || '';
|
||||
const referrer = request.headers.get('referer') || '';
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || '127.0.0.1';
|
||||
|
||||
// Basic User-Agent parsing
|
||||
const isMobile = /mobile/i.test(userAgent);
|
||||
const device = isMobile ? 'Mobile' : 'Desktop';
|
||||
|
||||
let browser = 'Other';
|
||||
if (/chrome|crios/i.test(userAgent)) browser = 'Chrome';
|
||||
else if (/firefox|fxios/i.test(userAgent)) browser = 'Firefox';
|
||||
else if (/safari/i.test(userAgent)) browser = 'Safari';
|
||||
else if (/edge/i.test(userAgent)) browser = 'Edge';
|
||||
|
||||
let os = 'Other';
|
||||
if (/windows/i.test(userAgent)) os = 'Windows';
|
||||
else if (/macintosh/i.test(userAgent)) os = 'macOS';
|
||||
else if (/android/i.test(userAgent)) os = 'Android';
|
||||
else if (/iphone|ipad/i.test(userAgent)) os = 'iOS';
|
||||
|
||||
// Simple Country detection
|
||||
const { country, city } = await getGeoInfo(ip, request.headers);
|
||||
|
||||
const event = await prisma.analyticsEvent.create({
|
||||
data: {
|
||||
type,
|
||||
@@ -19,6 +42,13 @@ export async function POST(request: NextRequest) {
|
||||
label: label || null,
|
||||
value: value || null,
|
||||
metadata: metadata || {},
|
||||
ip,
|
||||
country,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
referrer,
|
||||
userId: metadata?.userId || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../../lib/prisma';
|
||||
import prisma from '@/lib/prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../../lib/prisma';
|
||||
import prisma from '@/lib/prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/blog
|
||||
export async function GET() {
|
||||
|
||||
115
app/api/businesses/[id]/rate/route.ts
Normal file
115
app/api/businesses/[id]/rate/route.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id: paramId } = await context.params;
|
||||
const userId = req.headers.get('x-user-id');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { value, comment } = await req.json();
|
||||
|
||||
if (!value || value < 1 || value > 5) {
|
||||
return NextResponse.json({ error: 'Valeur de note invalide (1-5)' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Resolve actual Business UUID if paramId is a slug
|
||||
const business = await prisma.business.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: paramId },
|
||||
{ slug: paramId }
|
||||
]
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!business) {
|
||||
return NextResponse.json({ error: "Les données de démonstration ne peuvent pas recevoir d'avis. Veuillez créer votre propre entreprise pour tester." }, { status: 403 });
|
||||
}
|
||||
|
||||
const businessId = business.id;
|
||||
const ownerId = (business as any).ownerId;
|
||||
|
||||
// 2. Security: Don't allow owner to rate themselves
|
||||
if (userId === ownerId) {
|
||||
return NextResponse.json({ error: "Vous ne pouvez pas noter votre propre entreprise." }, { status: 403 });
|
||||
}
|
||||
|
||||
// 3. Fetch existing rating to check rules
|
||||
const existingRating = await prisma.rating.findUnique({
|
||||
where: {
|
||||
userId_businessId: {
|
||||
userId,
|
||||
businessId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existingRating) {
|
||||
// Rule: Score can only be re-evaluated UPWARDS
|
||||
if (value < existingRating.value) {
|
||||
return NextResponse.json({
|
||||
error: `La note ne peut être réévaluée qu'à la hausse. Votre note actuelle est de ${existingRating.value} étoiles.`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Rule: Comment is IMMUTABLE once set
|
||||
// We only update the value. If the comment was empty, we can set it once.
|
||||
await prisma.rating.update({
|
||||
where: { id: existingRating.id },
|
||||
data: {
|
||||
value,
|
||||
comment: existingRating.comment ? undefined : comment, // Only set if it was null
|
||||
status: 'PENDING' // Reset to pending for re-moderation
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 4. Create new rating
|
||||
await prisma.rating.create({
|
||||
data: {
|
||||
value,
|
||||
comment,
|
||||
userId,
|
||||
businessId,
|
||||
status: 'PENDING'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Aggregate all ratings for this business
|
||||
const stats = await prisma.rating.aggregate({
|
||||
where: { businessId },
|
||||
_avg: { value: true },
|
||||
_count: { value: true }
|
||||
});
|
||||
|
||||
const newRating = stats._avg.value || 0;
|
||||
const newRatingCount = stats._count.value || 0;
|
||||
|
||||
// 4. Update the business
|
||||
const updatedBusiness = await prisma.business.update({
|
||||
where: { id: businessId },
|
||||
data: {
|
||||
rating: newRating,
|
||||
ratingCount: newRatingCount
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
rating: updatedBusiness.rating,
|
||||
ratingCount: updatedBusiness.ratingCount,
|
||||
message: 'Vote enregistré avec succès'
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error rating business:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur lors du vote' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
49
app/api/businesses/[id]/rating/me/route.ts
Normal file
49
app/api/businesses/[id]/rating/me/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id: paramId } = await context.params;
|
||||
const userId = req.headers.get('x-user-id');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ rating: null });
|
||||
}
|
||||
|
||||
try {
|
||||
// Resolve actual Business UUID if paramId is a slug
|
||||
const business = await prisma.business.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: paramId },
|
||||
{ slug: paramId }
|
||||
]
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!business) {
|
||||
return NextResponse.json({ rating: null });
|
||||
}
|
||||
|
||||
const rating = await prisma.rating.findUnique({
|
||||
where: {
|
||||
userId_businessId: {
|
||||
userId,
|
||||
businessId: business.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
rating: rating ? rating.value : null,
|
||||
status: rating ? rating.status : null,
|
||||
comment: rating ? rating.comment : null
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching user rating:', error);
|
||||
return NextResponse.json({ error: 'Erreur lors de la récupération de votre note' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
55
app/api/businesses/[id]/ratings/route.ts
Normal file
55
app/api/businesses/[id]/ratings/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id: paramId } = await context.params;
|
||||
|
||||
try {
|
||||
// 1. Resolve actual Business UUID if paramId is a slug
|
||||
const business = await prisma.business.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id: paramId },
|
||||
{ slug: paramId }
|
||||
]
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!business) {
|
||||
// Return empty array for mock/missing businesses instead of error
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
const businessId = business.id;
|
||||
|
||||
// 2. Fetch all approved ratings with user info
|
||||
const ratings = await prisma.rating.findMany({
|
||||
where: {
|
||||
businessId,
|
||||
status: 'APPROVED'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
avatar: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(ratings);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching ratings:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur lors de la récupération des avis' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
|
||||
@@ -49,12 +49,48 @@ export async function PATCH(
|
||||
|
||||
// DELETE /api/businesses/[id]
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const { id } = await context.params
|
||||
const headerUserId = request.headers.get('x-user-id')
|
||||
|
||||
// 1. Find business to check owner
|
||||
const business = await prisma.business.findUnique({
|
||||
where: { id },
|
||||
select: { ownerId: true }
|
||||
})
|
||||
|
||||
if (!business) {
|
||||
return NextResponse.json({ error: 'Boutique non trouvée' }, { status: 404 })
|
||||
}
|
||||
|
||||
// 2. Auth check (Owner or Admin)
|
||||
const user = headerUserId ? await prisma.user.findUnique({ where: { id: headerUserId } }) : null
|
||||
const isAdmin = user?.role === 'ADMIN'
|
||||
const isOwner = business.ownerId === headerUserId
|
||||
|
||||
if (!isOwner && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
|
||||
}
|
||||
|
||||
// 3. Delete business
|
||||
await prisma.business.delete({ where: { id } })
|
||||
|
||||
// 4. Update user role if they don't have other businesses
|
||||
if (headerUserId && !isAdmin) {
|
||||
const otherBusinesses = await prisma.business.findFirst({
|
||||
where: { ownerId: headerUserId }
|
||||
})
|
||||
if (!otherBusinesses) {
|
||||
await prisma.user.update({
|
||||
where: { id: headerUserId },
|
||||
data: { role: 'VISITOR' }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/businesses/[id] error:', error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Force refresh for Next.js 16 params fix
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../../../lib/prisma';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
@@ -13,10 +13,27 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'ID requis' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Attempt to increment in DB
|
||||
// Note: We don't care about mock businesses here as they are static
|
||||
// 1. Resolve actual Business UUID if id is a slug
|
||||
const businessFound = await prisma.business.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id },
|
||||
{ slug: id }
|
||||
]
|
||||
},
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!businessFound) {
|
||||
// If business not found in DB (might be mock), just return success but no update
|
||||
return NextResponse.json({ success: true, message: 'Mock business, skipping view count' });
|
||||
}
|
||||
|
||||
const businessId = businessFound.id;
|
||||
|
||||
// 2. Attempt to increment in DB
|
||||
const business = await prisma.business.update({
|
||||
where: { id },
|
||||
where: { id: businessId },
|
||||
data: {
|
||||
viewCount: {
|
||||
increment: 1,
|
||||
@@ -26,8 +43,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json({ success: true, viewCount: business.viewCount });
|
||||
} catch (error) {
|
||||
// If business not found in DB (might be mock), just return success but no update
|
||||
console.error('Error incrementing view count:', error);
|
||||
return NextResponse.json({ success: false, error: 'Business not found or error' }, { status: 404 });
|
||||
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/businesses/me — Fetch business for the current user
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import { MOCK_BUSINESSES } from '../../../lib/mockData'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/businesses — List all businesses (Mock + DB)
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -9,6 +8,7 @@ export async function GET(request: NextRequest) {
|
||||
const category = searchParams.get('category')
|
||||
const q = searchParams.get('q')?.toLowerCase()
|
||||
const featured = searchParams.get('featured')
|
||||
const countryId = searchParams.get('countryId')
|
||||
|
||||
// 1. Fetch from Database
|
||||
const where: any = {
|
||||
@@ -20,6 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
if (category && category !== 'All') where.category = category
|
||||
if (featured === 'true') where.isFeatured = true
|
||||
if (countryId) where.countryId = countryId
|
||||
if (q) {
|
||||
where.OR = [
|
||||
{ name: { contains: q, mode: 'insensitive' } },
|
||||
@@ -35,31 +36,8 @@ export async function GET(request: NextRequest) {
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// 2. Filter Mock Businesses
|
||||
const filteredMocks = MOCK_BUSINESSES.filter(biz => {
|
||||
// Category filter
|
||||
if (category && category !== 'All' && biz.category !== category) return false;
|
||||
|
||||
// Featured filter
|
||||
if (featured === 'true' && !biz.isFeatured) return false;
|
||||
|
||||
// Search query filter
|
||||
if (q) {
|
||||
const matches =
|
||||
biz.name.toLowerCase().includes(q) ||
|
||||
biz.description.toLowerCase().includes(q) ||
|
||||
biz.tags.some(t => t.toLowerCase().includes(q));
|
||||
if (!matches) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 3. Combine and return
|
||||
// Prioritize DB entries over mocks to show user-created data first
|
||||
const combined = [...dbBusinesses, ...filteredMocks];
|
||||
|
||||
return NextResponse.json(combined)
|
||||
// 2. Return DB results
|
||||
return NextResponse.json(dbBusinesses)
|
||||
} catch (error) {
|
||||
console.error('GET /api/businesses error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
@@ -84,6 +62,8 @@ export async function POST(request: NextRequest) {
|
||||
slug: data.slug || null,
|
||||
category: data.category || "Autre",
|
||||
location: data.location || "",
|
||||
countryId: data.countryId || null,
|
||||
city: data.city || "",
|
||||
description: data.description || "",
|
||||
logoUrl: data.logoUrl || "https://picsum.photos/200/200?random=default",
|
||||
videoUrl: data.videoUrl || null,
|
||||
@@ -107,7 +87,7 @@ export async function POST(request: NextRequest) {
|
||||
// 1b. Calculate isActive based on mandatory fields
|
||||
const isNameOk = cleanData.name && cleanData.name !== "Nouvelle Entreprise";
|
||||
const isDescOk = cleanData.description && cleanData.description.length >= 20;
|
||||
const isLocOk = cleanData.location && cleanData.location !== "Ma Ville";
|
||||
const isLocOk = (cleanData.countryId && cleanData.city) || (cleanData.location && cleanData.location !== "Ma Ville");
|
||||
const isEmailOk = !!cleanData.contactEmail;
|
||||
const isPhoneOk = !!cleanData.contactPhone;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/conversations — List all conversations for the user
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
15
app/api/countries/route.ts
Normal file
15
app/api/countries/route.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const countries = await prisma.country.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
return NextResponse.json(countries);
|
||||
} catch (error) {
|
||||
console.error('GET /api/countries error:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (action === 'description') {
|
||||
const prompt = `
|
||||
Tu es un expert en copywriting marketing pour Afropreunariat.
|
||||
Tu es un expert en copywriting marketing pour Afrohub.
|
||||
Rédige une description professionnelle, attrayante et optimisée SEO pour une entreprise.
|
||||
|
||||
Nom de l'entreprise : ${name}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/interviews
|
||||
export async function GET() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// POST /api/messages — Send a message or start a conversation
|
||||
export async function POST(request: NextRequest) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/offers — optionally filter by businessId
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -26,6 +26,44 @@ export async function GET(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { businessId } = body
|
||||
|
||||
if (!businessId) {
|
||||
return NextResponse.json({ error: 'businessId manquant' }, { status: 400 })
|
||||
}
|
||||
|
||||
// 1. Fetch business and their plan's limit
|
||||
const business = await prisma.business.findUnique({
|
||||
where: { id: businessId },
|
||||
include: {
|
||||
_count: { select: { offers: true } }
|
||||
}
|
||||
})
|
||||
|
||||
if (!business) {
|
||||
return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Fetch the specific limit for this tier from the database
|
||||
const planDetails = await prisma.pricingPlan.findUnique({
|
||||
where: { tier: business.plan }
|
||||
})
|
||||
|
||||
if (!planDetails) {
|
||||
return NextResponse.json({ error: 'Configuration du forfait introuvable' }, { status: 500 })
|
||||
}
|
||||
|
||||
// 2. Enforce limits
|
||||
const currentCount = business._count.offers
|
||||
const offerLimit = planDetails.offerLimit
|
||||
|
||||
if (currentCount >= offerLimit) {
|
||||
return NextResponse.json({
|
||||
error: `Limite d'offres atteinte (${offerLimit} max pour le plan ${planDetails.name}). Veuillez passer au plan supérieur pour publier plus d'offres.`
|
||||
}, { status: 403 })
|
||||
}
|
||||
|
||||
// 3. Create offer
|
||||
const offer = await prisma.offer.create({
|
||||
data: body,
|
||||
include: { business: true },
|
||||
|
||||
14
app/api/plans/route.ts
Normal file
14
app/api/plans/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const plans = await prisma.pricingPlan.findMany({
|
||||
orderBy: { offerLimit: 'asc' }
|
||||
});
|
||||
return NextResponse.json(plans);
|
||||
} catch (error) {
|
||||
console.error('GET /api/plans error:', error);
|
||||
return NextResponse.json({ error: 'Erreur lors du chargement des plans' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
62
app/api/ratings/[id]/reply/route.ts
Normal file
62
app/api/ratings/[id]/reply/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id: ratingId } = await context.params;
|
||||
const userId = req.headers.get('x-user-id');
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { reply } = await req.json();
|
||||
|
||||
if (!reply || reply.trim() === '') {
|
||||
return NextResponse.json({ error: 'La réponse ne peut pas être vide.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Fetch the rating to identify the business
|
||||
const rating = await prisma.rating.findUnique({
|
||||
where: { id: ratingId },
|
||||
include: {
|
||||
business: {
|
||||
select: {
|
||||
ownerId: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!rating) {
|
||||
return NextResponse.json({ error: 'Avis introuvable.' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 2. Security: Only the owner of the business can reply
|
||||
if (rating.business.ownerId !== userId) {
|
||||
return NextResponse.json({ error: 'Action non autorisée. Seul l\'entrepreneur concerné peut répondre.' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 3. Update the rating with the reply
|
||||
const updatedRating = await prisma.rating.update({
|
||||
where: { id: ratingId },
|
||||
data: {
|
||||
reply,
|
||||
replyAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
reply: updatedRating.reply,
|
||||
replyAt: updatedRating.replyAt
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error replying to rating:', error);
|
||||
return NextResponse.json({ error: 'Erreur serveur lors de la réponse.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// PATCH /api/users/me — Update personal user profile
|
||||
export async function PATCH(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '../../../../lib/prisma';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// GET /api/users
|
||||
export async function GET() {
|
||||
|
||||
Reference in New Issue
Block a user