Implement Review Lifecycle Tracking

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 20:28:35 +00:00
parent 7dcf8156b9
commit f36ef04a54
4 changed files with 405 additions and 4 deletions

View File

@@ -13,7 +13,9 @@ export type ActivityType =
| 'account_deletion_confirmed'
| 'account_deletion_cancelled'
| 'user_banned'
| 'user_unbanned';
| 'user_unbanned'
| 'review_created'
| 'review_deleted';
export interface ActivityActor {
id: string;
@@ -84,6 +86,19 @@ export interface AccountLifecycleDetails {
request_id?: string;
}
export interface ReviewLifecycleDetails {
review_id: string;
user_id: string;
username?: string;
entity_type: 'park' | 'ride';
entity_id: string;
entity_name?: string;
rating?: number;
review_text?: string;
deletion_reason?: string;
was_moderated?: boolean;
}
export type ActivityDetails =
| EntityChangeDetails
| AdminActionDetails
@@ -91,7 +106,8 @@ export type ActivityDetails =
| ReportResolutionDetails
| ReviewModerationDetails
| PhotoApprovalDetails
| AccountLifecycleDetails;
| AccountLifecycleDetails
| ReviewLifecycleDetails;
export interface SystemActivity {
id: string;
@@ -572,6 +588,70 @@ export async function fetchSystemActivities(
}
}
// Fetch review lifecycle events
// 1. Review creations (recent 7 days)
const { data: newReviews, error: reviewsError } = await supabase
.from('reviews')
.select('id, user_id, park_id, ride_id, rating, review_text, created_at')
.gte('created_at', sevenDaysAgo.toISOString())
.order('created_at', { ascending: false })
.limit(Math.ceil(limit / 2));
if (!reviewsError && newReviews) {
for (const review of newReviews) {
const entityType = review.park_id ? 'park' : 'ride';
const entityId = review.park_id || review.ride_id;
activities.push({
id: `review-${review.id}`,
type: 'review_created',
timestamp: review.created_at,
actor_id: review.user_id,
action: 'created review',
details: {
review_id: review.id,
user_id: review.user_id,
entity_type: entityType as 'park' | 'ride',
entity_id: entityId!,
rating: review.rating,
review_text: review.review_text,
} as ReviewLifecycleDetails,
});
}
}
// 2. Review deletions
const { data: deletedReviews, error: deletionsError } = await supabase
.from('review_deletions')
.select('id, review_id, user_id, park_id, ride_id, rating, review_text, deleted_by, deleted_at, deletion_reason, was_moderated')
.order('deleted_at', { ascending: false })
.limit(limit);
if (!deletionsError && deletedReviews) {
for (const deletion of deletedReviews) {
const entityType = deletion.park_id ? 'park' : 'ride';
const entityId = deletion.park_id || deletion.ride_id;
activities.push({
id: deletion.id,
type: 'review_deleted',
timestamp: deletion.deleted_at!,
actor_id: deletion.deleted_by,
action: 'deleted review',
details: {
review_id: deletion.review_id,
user_id: deletion.user_id,
entity_type: entityType as 'park' | 'ride',
entity_id: entityId!,
rating: deletion.rating,
review_text: deletion.review_text || undefined,
deletion_reason: deletion.deletion_reason || undefined,
was_moderated: deletion.was_moderated,
} as ReviewLifecycleDetails,
});
}
}
// Sort all activities by timestamp (newest first)
activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
@@ -670,6 +750,86 @@ export async function fetchSystemActivities(
}
}
}
// Enrich review lifecycle users and entities
const reviewUserIds = filteredActivities
.filter(a => ['review_created', 'review_deleted'].includes(a.type))
.map(a => (a.details as ReviewLifecycleDetails).user_id)
.filter(Boolean) as string[];
if (reviewUserIds.length > 0) {
const { data: reviewProfiles } = await supabase
.from('profiles')
.select('user_id, username')
.in('user_id', reviewUserIds);
if (reviewProfiles) {
const reviewProfileMap = new Map(reviewProfiles.map(p => [p.user_id, p]));
for (const activity of filteredActivities) {
if (['review_created', 'review_deleted'].includes(activity.type)) {
const details = activity.details as ReviewLifecycleDetails;
if (details.user_id && !details.username) {
const reviewProfile = reviewProfileMap.get(details.user_id);
if (reviewProfile) {
details.username = reviewProfile.username;
}
}
}
}
}
}
// Enrich review entity names
const parkReviewIds = filteredActivities
.filter(a => ['review_created', 'review_deleted'].includes(a.type) && (a.details as ReviewLifecycleDetails).entity_type === 'park')
.map(a => (a.details as ReviewLifecycleDetails).entity_id)
.filter(Boolean) as string[];
const rideReviewIds = filteredActivities
.filter(a => ['review_created', 'review_deleted'].includes(a.type) && (a.details as ReviewLifecycleDetails).entity_type === 'ride')
.map(a => (a.details as ReviewLifecycleDetails).entity_id)
.filter(Boolean) as string[];
if (parkReviewIds.length > 0) {
const { data: parks } = await supabase
.from('parks')
.select('id, name')
.in('id', parkReviewIds);
if (parks) {
const parkMap = new Map(parks.map(p => [p.id, p.name]));
for (const activity of filteredActivities) {
if (['review_created', 'review_deleted'].includes(activity.type)) {
const details = activity.details as ReviewLifecycleDetails;
if (details.entity_type === 'park' && !details.entity_name) {
details.entity_name = parkMap.get(details.entity_id);
}
}
}
}
}
if (rideReviewIds.length > 0) {
const { data: rides } = await supabase
.from('rides')
.select('id, name')
.in('id', rideReviewIds);
if (rides) {
const rideMap = new Map(rides.map(r => [r.id, r.name]));
for (const activity of filteredActivities) {
if (['review_created', 'review_deleted'].includes(activity.type)) {
const details = activity.details as ReviewLifecycleDetails;
if (details.entity_type === 'ride' && !details.entity_name) {
details.entity_name = rideMap.get(details.entity_id);
}
}
}
}
}
}
}