create review and profile

This commit is contained in:
arifal
2026-04-20 00:25:05 +07:00
parent 7159e9108f
commit ba5f64ae0e
37 changed files with 3324 additions and 109 deletions
+51
View File
@@ -0,0 +1,51 @@
import { userRepo } from "@/server/repositories/user.repo";
import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
export const profileService = {
async getProfileDashboard(userId: string) {
const user = await userRepo.findPublicProfileById(userId);
if (!user) {
throw new Error("Pengguna tidak ditemukan");
}
const [organizedTrips, participations] = await Promise.all([
tripRepo.findByOrganizerId(userId),
participantRepo.findWithTripForProfile(userId),
]);
const activeJoined = participations
.filter((p) => p.status !== "CANCELLED")
.sort(
(a, b) =>
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
);
const cancelledJoined = participations
.filter((p) => p.status === "CANCELLED")
.sort(
(a, b) =>
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
);
const reviewable = activeJoined
.filter((p) => {
if (p.status !== "CONFIRMED") return false;
const t = p.trip;
if (t.organizerId === userId) return false;
return isPastTripLastDayForReview(t.date, t.endDate);
})
.sort(
(a, b) =>
new Date(b.trip.date).getTime() - new Date(a.trip.date).getTime()
);
return {
user,
organizedTrips,
activeJoined,
cancelledJoined,
reviewable,
};
},
};
+44
View File
@@ -0,0 +1,44 @@
import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { reviewRepo } from "@/server/repositories/review.repo";
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
export const reviewService = {
async upsertReview(
tripId: string,
userId: string,
input: { rating: number; comment?: string | null }
) {
const trip = await tripRepo.findById(tripId);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (trip.organizerId === userId) {
throw new Error("Organizer tidak bisa mengulas trip sendiri");
}
const participation = await participantRepo.findByTripAndUser(
tripId,
userId
);
if (!participation || participation.status !== "CONFIRMED") {
throw new Error(
"Hanya peserta yang terdaftar (aktif) yang bisa memberi ulasan"
);
}
if (!isPastTripLastDayForReview(trip.date, trip.endDate)) {
throw new Error(
"Ulasan bisa diberikan setelah tanggal selesai trip (hari terakhir pendakian)"
);
}
return reviewRepo.upsert({
tripId,
userId,
rating: input.rating,
comment: input.comment ?? null,
});
},
};
+43 -3
View File
@@ -1,5 +1,7 @@
import { tripRepo } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
interface CreateTripInput {
title: string;
@@ -32,6 +34,25 @@ export const tripService = {
},
async createTrip(input: CreateTripInput) {
const since = utcStartOfDay(new Date());
const todayCount = await tripRepo.countByOrganizerSince(
input.organizerId,
since
);
if (todayCount >= LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY) {
throw new Error(
`Batas harian: maksimal ${LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY} trip per hari (UTC). Coba lagi besok.`
);
}
if (isTripDepartureDayPast(input.date)) {
throw new Error("Tanggal berangkat tidak boleh di masa lalu");
}
if (input.endDate && input.endDate.getTime() < input.date.getTime()) {
throw new Error("Tanggal pulang tidak boleh sebelum tanggal berangkat");
}
const images = input.imageUrls?.length
? {
create: input.imageUrls.map((url, i) => ({ url, order: i })),
@@ -62,6 +83,12 @@ export const tripService = {
throw new Error("Trip tidak tersedia untuk pendaftaran");
}
if (isTripDepartureDayPast(trip.date)) {
throw new Error(
"Trip sudah melewati tanggal berangkat, tidak bisa mendaftar"
);
}
if (trip.organizerId === userId) {
throw new Error("Organizer tidak bisa join trip sendiri");
}
@@ -77,7 +104,10 @@ export const tripService = {
throw new Error("Trip sudah penuh");
}
const participant = await participantRepo.create(tripId, userId);
const participant =
existing?.status === "CANCELLED"
? await participantRepo.reactivate(tripId, userId)
: await participantRepo.create(tripId, userId);
const newCount = await participantRepo.countByTrip(tripId);
if (newCount >= trip.maxParticipants) {
@@ -88,6 +118,17 @@ export const tripService = {
},
async cancelJoin(tripId: string, userId: string) {
const trip = await tripRepo.findById(tripId);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
if (isTripDepartureDayPast(trip.date)) {
throw new Error(
"Tanggal berangkat trip sudah lewat — pendaftaran tidak bisa dibatalkan. Jika trip sudah selesai, gunakan bagian ulasan."
);
}
const existing = await participantRepo.findByTripAndUser(tripId, userId);
if (!existing || existing.status === "CANCELLED") {
throw new Error("Kamu tidak terdaftar di trip ini");
@@ -95,8 +136,7 @@ export const tripService = {
const result = await participantRepo.cancel(tripId, userId);
const trip = await tripRepo.findById(tripId);
if (trip && trip.status === "FULL") {
if (trip.status === "FULL") {
const count = await participantRepo.countByTrip(tripId);
if (count < trip.maxParticipants) {
await tripRepo.updateStatus(tripId, "OPEN");