create payment roadmap pr a
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface CopyButtonProps {
|
||||
value: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function CopyButton({ value, label = "Salin" }: CopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function handleClick() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch {
|
||||
// ignore — user can copy manually
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="rounded-lg border border-neutral-200 bg-white px-2.5 py-1 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
|
||||
>
|
||||
{copied ? "✓ Tersalin" : label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { markParticipantPaidAction } from "@/features/booking/actions";
|
||||
|
||||
interface MarkPaidButtonProps {
|
||||
tripId: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function MarkPaidButton({ tripId, disabled }: MarkPaidButtonProps) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function handleClick() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const result = await markParticipantPaidAction(tripId);
|
||||
setLoading(false);
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
return;
|
||||
}
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<div className="mb-3 rounded-xl bg-red-50 px-4 py-3 text-sm font-medium text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={loading || disabled}
|
||||
className="w-full rounded-xl bg-primary-600 py-3 text-sm font-bold text-white shadow-lg shadow-primary-600/20 transition-colors hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Memproses..." : "Saya sudah bayar"}
|
||||
</button>
|
||||
<p className="mt-2 text-center text-[11px] text-neutral-500">
|
||||
Tekan setelah kamu transfer ke rekening di atas. Organizer akan cek &
|
||||
konfirmasi pembayaran kamu.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -63,7 +63,8 @@ export function ReviewCard({ verification }: { verification: Verification }) {
|
||||
{verification.user.name}
|
||||
</h3>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{verification.user.email} · diajukan {formatDate(verification.createdAt)}
|
||||
{verification.user.email} · diajukan{" "}
|
||||
{formatDate(verification.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<StatusPill status={verification.status} />
|
||||
@@ -80,10 +81,7 @@ export function ReviewCard({ verification }: { verification: Verification }) {
|
||||
label="Bank"
|
||||
value={`${verification.bankName} · ${verification.bankAccountNumber}`}
|
||||
/>
|
||||
<Field
|
||||
label="Pemilik Rekening"
|
||||
value={verification.bankAccountName}
|
||||
/>
|
||||
<Field label="Pemilik Rekening" value={verification.bankAccountName} />
|
||||
<Field label="Alamat" value={verification.address} fullWidth />
|
||||
</div>
|
||||
|
||||
@@ -212,24 +210,30 @@ function ImagePreview({ label, src }: { label: string; src: string }) {
|
||||
rel="noopener noreferrer"
|
||||
className="block overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100"
|
||||
>
|
||||
<div className="relative aspect-[4/3] w-full">
|
||||
<div className="relative aspect-4/3 w-full">
|
||||
{/* Secure endpoint sends Cache-Control: private,no-store. Use plain <img> to skip Next/Image optimizer. */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={label}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<img src={src} alt={label} className="h-full w-full object-cover" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPill({ status }: { status: "PENDING" | "APPROVED" | "REJECTED" }) {
|
||||
function StatusPill({
|
||||
status,
|
||||
}: {
|
||||
status: "PENDING" | "APPROVED" | "REJECTED";
|
||||
}) {
|
||||
const cfg = {
|
||||
PENDING: { label: "Pending", cls: "bg-amber-50 text-amber-700 ring-amber-200" },
|
||||
APPROVED: { label: "Disetujui", cls: "bg-primary-50 text-primary-700 ring-primary-200" },
|
||||
PENDING: {
|
||||
label: "Pending",
|
||||
cls: "bg-amber-50 text-amber-700 ring-amber-200",
|
||||
},
|
||||
APPROVED: {
|
||||
label: "Disetujui",
|
||||
cls: "bg-primary-50 text-primary-700 ring-primary-200",
|
||||
},
|
||||
REJECTED: { label: "Ditolak", cls: "bg-red-50 text-red-700 ring-red-200" },
|
||||
}[status];
|
||||
return (
|
||||
|
||||
@@ -4,16 +4,17 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { joinTripAction, cancelJoinAction } from "@/features/trip/actions";
|
||||
import { markParticipantPaidAction } from "@/features/booking/actions";
|
||||
|
||||
interface JoinTripButtonProps {
|
||||
tripId: string;
|
||||
isLoggedIn: boolean;
|
||||
isOrganizer: boolean;
|
||||
isJoined: boolean;
|
||||
/** Trip gratis (price <= 0) — sembunyikan flow pembayaran */
|
||||
isFree: boolean;
|
||||
/** Status partisipasi user saat isJoined (bukan organizer) */
|
||||
participationStatus?: "PENDING" | "CONFIRMED" | null;
|
||||
/** Status pembayaran manual (peserta) */
|
||||
/** Status pembayaran manual (peserta). Hanya relevan untuk trip berbayar. */
|
||||
participantPayment?: {
|
||||
markedPaidAt: string | Date | null;
|
||||
paymentConfirmedAt: string | Date | null;
|
||||
@@ -29,6 +30,7 @@ export function JoinTripButton({
|
||||
isLoggedIn,
|
||||
isOrganizer,
|
||||
isJoined,
|
||||
isFree,
|
||||
participationStatus,
|
||||
participantPayment,
|
||||
isFull,
|
||||
@@ -108,28 +110,11 @@ export function JoinTripButton({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMarkPaid() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const result = await markParticipantPaidAction(tripId);
|
||||
setLoading(false);
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
const pay = participantPayment;
|
||||
const showMarkPaid =
|
||||
isJoined &&
|
||||
pay &&
|
||||
!pay.paymentConfirmedAt &&
|
||||
!pay.markedPaidAt &&
|
||||
!isDeparturePast;
|
||||
const showPaymentLink = !isFree && isJoined && !isDeparturePast;
|
||||
const waitingPaymentConfirm =
|
||||
isJoined && pay && pay.markedPaidAt && !pay.paymentConfirmedAt;
|
||||
const paymentDone = isJoined && pay && pay.paymentConfirmedAt;
|
||||
!isFree && isJoined && pay && pay.markedPaidAt && !pay.paymentConfirmedAt;
|
||||
const paymentDone = !isFree && isJoined && pay && pay.paymentConfirmedAt;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -149,7 +134,8 @@ export function JoinTripButton({
|
||||
<div className="mb-3 rounded-xl border border-secondary-200 bg-secondary-50 px-4 py-3 text-sm font-medium text-secondary-900">
|
||||
Kamu sudah{" "}
|
||||
<span className="font-semibold">terkonfirmasi</span> sebagai peserta
|
||||
trip ini.
|
||||
trip ini
|
||||
{isFree && <span> — trip gratis, tidak ada pembayaran 🎉</span>}.
|
||||
</div>
|
||||
)}
|
||||
{waitingPaymentConfirm && (
|
||||
@@ -164,15 +150,17 @@ export function JoinTripButton({
|
||||
<span className="font-semibold">dikonfirmasi organizer</span>.
|
||||
</div>
|
||||
)}
|
||||
{showMarkPaid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMarkPaid}
|
||||
disabled={loading}
|
||||
className="mb-3 w-full rounded-xl border-2 border-primary-500 bg-white py-3 text-sm font-bold text-primary-700 transition-colors hover:bg-primary-50 disabled:opacity-50"
|
||||
{showPaymentLink && (
|
||||
<Link
|
||||
href={`/trips/${tripId}/payment`}
|
||||
className="mb-3 block w-full rounded-xl border-2 border-primary-500 bg-white py-3 text-center text-sm font-bold text-primary-700 transition-colors hover:bg-primary-50"
|
||||
>
|
||||
{loading ? "Memproses..." : "Saya sudah bayar"}
|
||||
</button>
|
||||
{paymentDone
|
||||
? "Lihat detail pembayaran"
|
||||
: pay?.markedPaidAt
|
||||
? "Lihat status pembayaran"
|
||||
: "Buka detail pembayaran"}
|
||||
</Link>
|
||||
)}
|
||||
{isJoined ? (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user