add payment, trust badge, handle race condition, fix booking schema
This commit is contained in:
+94
-8
@@ -3,8 +3,14 @@ import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { tripService } from "@/server/services/trip.service";
|
||||
import { formatRupiah, formatDateRange } from "@/lib/utils";
|
||||
import { trustService } from "@/server/services/trust.service";
|
||||
import { formatRupiah } from "@/lib/utils";
|
||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
||||
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
|
||||
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
|
||||
import { TripProgramBlock } from "@/features/trip/components/trip-program-block";
|
||||
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
||||
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
||||
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||
import {
|
||||
@@ -27,10 +33,21 @@ export default async function TripDetailPage({
|
||||
notFound();
|
||||
}
|
||||
|
||||
const organizerTrust = await trustService.getOrganizerTrust(
|
||||
trip.organizerId
|
||||
);
|
||||
|
||||
const activeParticipants = trip.participants.filter(
|
||||
(p) => p.status !== "CANCELLED"
|
||||
);
|
||||
const confirmedParticipants = activeParticipants.filter(
|
||||
(p) => p.status === "CONFIRMED"
|
||||
);
|
||||
const pendingParticipants = activeParticipants.filter(
|
||||
(p) => p.status === "PENDING"
|
||||
);
|
||||
const participantCount = activeParticipants.length;
|
||||
const confirmedCount = confirmedParticipants.length;
|
||||
const spotsLeft = trip.maxParticipants - participantCount;
|
||||
const fillPercent = Math.min(
|
||||
(participantCount / trip.maxParticipants) * 100,
|
||||
@@ -63,6 +80,10 @@ export default async function TripDetailPage({
|
||||
) / 10
|
||||
: null;
|
||||
|
||||
const paymentPendingParticipants = activeParticipants.filter(
|
||||
(p) => p.markedPaidAt && !p.paymentConfirmedAt
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-4 sm:py-8">
|
||||
{/* Breadcrumb */}
|
||||
@@ -123,7 +144,7 @@ export default async function TripDetailPage({
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-medium text-neutral-400 sm:text-xs">Tanggal</p>
|
||||
<p className="truncate text-xs font-semibold text-neutral-800 sm:text-sm">
|
||||
{formatDateRange(trip.date, trip.endDate)}
|
||||
{formatTripCalendarDateRangeLong(trip.date, trip.endDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,6 +174,12 @@ export default async function TripDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OrganizerTrustPanel
|
||||
name={trip.organizer.name}
|
||||
image={trip.organizer.image}
|
||||
trust={organizerTrust}
|
||||
/>
|
||||
|
||||
{/* Participant Progress */}
|
||||
<div className="rounded-xl border border-neutral-200 p-3 sm:p-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
@@ -179,12 +206,30 @@ export default async function TripDetailPage({
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-neutral-500 sm:text-xs">
|
||||
Maksimal {trip.maxParticipants} orang. Saat ini {participantCount}{" "}
|
||||
mendaftar, {confirmedCount} sudah disetujui organizer.
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-neutral-500 sm:text-xs">
|
||||
{spotsLeft > 0
|
||||
? `${spotsLeft} slot tersisa — yuk gabung!`
|
||||
? `Masih ada ${spotsLeft} tempat — yuk gabung!`
|
||||
: "Trip sudah penuh"}
|
||||
{confirmedCount < participantCount && (
|
||||
<>
|
||||
{" "}
|
||||
· {participantCount - confirmedCount} menunggu persetujuan
|
||||
organizer
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TripProgramBlock
|
||||
meetingPoint={trip.meetingPoint}
|
||||
itinerary={trip.itinerary}
|
||||
whatsIncluded={trip.whatsIncluded}
|
||||
whatsExcluded={trip.whatsExcluded}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
{trip.description && (
|
||||
<div>
|
||||
@@ -197,12 +242,50 @@ export default async function TripDetailPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOrganizer && pendingParticipants.length > 0 && (
|
||||
<OrganizerJoinRequests
|
||||
tripId={trip.id}
|
||||
pending={pendingParticipants.map((p) => ({
|
||||
id: p.id,
|
||||
user: p.user,
|
||||
markedPaidAt: p.markedPaidAt,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOrganizer && paymentPendingParticipants.length > 0 && (
|
||||
<OrganizerPaymentQueue
|
||||
tripId={trip.id}
|
||||
items={paymentPendingParticipants.map((p) => ({
|
||||
id: p.id,
|
||||
user: p.user,
|
||||
joinStatus:
|
||||
p.status === "PENDING" ? ("PENDING" as const) : ("CONFIRMED" as const),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Action */}
|
||||
<JoinTripButton
|
||||
tripId={trip.id}
|
||||
isLoggedIn={!!session?.user}
|
||||
isOrganizer={isOrganizer}
|
||||
isJoined={!!currentParticipation}
|
||||
participationStatus={
|
||||
currentParticipation?.status === "PENDING" ||
|
||||
currentParticipation?.status === "CONFIRMED"
|
||||
? currentParticipation.status
|
||||
: null
|
||||
}
|
||||
participantPayment={
|
||||
currentParticipation
|
||||
? {
|
||||
markedPaidAt: currentParticipation.markedPaidAt,
|
||||
paymentConfirmedAt:
|
||||
currentParticipation.paymentConfirmedAt,
|
||||
}
|
||||
: null
|
||||
}
|
||||
isFull={spotsLeft <= 0}
|
||||
tripStatus={trip.status}
|
||||
isDeparturePast={isDeparturePast}
|
||||
@@ -226,18 +309,21 @@ export default async function TripDetailPage({
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Participants List */}
|
||||
{/* Peserta yang sudah disetujui organizer (publik) */}
|
||||
<div>
|
||||
<h2 className="mb-3 text-xs font-bold text-neutral-700 sm:text-sm">
|
||||
Peserta ({participantCount})
|
||||
Peserta terkonfirmasi ({confirmedCount})
|
||||
</h2>
|
||||
{participantCount === 0 ? (
|
||||
{confirmedCount === 0 ? (
|
||||
<p className="text-xs text-neutral-400 sm:text-sm">
|
||||
Belum ada peserta. Jadilah yang pertama! 🎒
|
||||
Belum ada peserta yang dikonfirmasi.{" "}
|
||||
{pendingParticipants.length > 0
|
||||
? "Cek permintaan join di atas untuk menyetujui peserta."
|
||||
: "Jadilah yang pertama mendaftar! 🎒"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{activeParticipants.map((p) => (
|
||||
{confirmedParticipants.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 sm:gap-2 sm:px-3 sm:py-1.5"
|
||||
|
||||
Reference in New Issue
Block a user