Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 107 additions & 37 deletions frontend/components/bookings/BookingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,37 @@ export default function BookingForm() {
: totalNaira;

async function handleApplyPromo() {
if (!promoCodeInput.trim() || !totalAmount) return;
if (!promoCodeInput.trim() || !totalAmount || !workspaceId) return;
setApplyingPromo(true);
setPromoError(null);
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:6001/api"}/promo-codes/validate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: promoCodeInput.trim(), subtotalKobo: totalAmount }),
body: JSON.stringify({
code: promoCodeInput.trim(),
workspaceId,
bookingAmount: totalAmount,
}),
});
const json = await res.json();
if (!res.ok || !json.valid) {
setPromoError(json.message || "Invalid promo code");
} else {
setAppliedPromo({ code: promoCodeInput.trim(), discountAmountKobo: json.discountAmountKobo, finalAmountKobo: json.finalAmountKobo });
if (!res.ok || !json?.valid) {
setPromoError(json?.message || "Invalid promo code");
return;
}
// Backend returns discountValue + finalAmount; convert to a kobo figure
// client-side for percentage discounts so we can display the actual savings.
const discountAmountKobo =
json.discountType === "PERCENTAGE"
? Math.floor((totalAmount * (json.discountValue ?? 0)) / 100)
: json.discountValue ?? 0;
const finalAmountKobo =
json.finalAmount ?? Math.max(0, totalAmount - discountAmountKobo);
setAppliedPromo({
code: promoCodeInput.trim(),
discountAmountKobo,
finalAmountKobo,
});
} catch {
setPromoError("Could not validate promo code");
} finally {
Expand Down Expand Up @@ -400,36 +416,6 @@ export default function BookingForm() {
</div>
</div>
)}
{!appliedPromo && (
<div>
<button
type="button"
onClick={() => setShowPromoField((v) => !v)}
className="text-xs text-gray-500 underline"
>
{showPromoField ? "Hide promo code" : "Have a promo code?"}
</button>
{showPromoField && (
<div className="mt-2 flex gap-2">
<input
value={promoCodeInput}
onChange={(e) => setPromoCodeInput(e.target.value)}
placeholder="Enter promo code"
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm"
/>
<button
type="button"
onClick={handleApplyPromo}
disabled={applyingPromo || !promoCodeInput.trim()}
className="px-3 py-2 text-sm bg-gray-900 text-white rounded-lg disabled:opacity-50"
>
{applyingPromo ? "..." : "Apply"}
</button>
</div>
)}
{promoError && <p className="text-xs text-red-500 mt-1">{promoError}</p>}
</div>
)}
</div>
)}

Expand Down Expand Up @@ -542,8 +528,92 @@ export default function BookingForm() {
</div>
<div className="flex justify-between">
<span className="text-gray-500">Amount</span>
<span className="font-bold text-gray-900">{totalNaira}</span>
<span className="font-bold text-gray-900">
{appliedPromo
? (appliedPromo.finalAmountKobo / 100).toLocaleString("en-NG", {
style: "currency",
currency: "NGN",
})
: totalNaira}
</span>
</div>
{appliedPromo && (
<div className="flex justify-between border-t border-gray-200 pt-2">
<span className="text-green-700">Discount ({appliedPromo.code})</span>
<button
onClick={() => {
setAppliedPromo(null);
setPromoCodeInput("");
}}
className="text-xs text-gray-500 underline"
>
Remove
</button>
</div>
)}
</div>

{/* Promo code (Payment step) */}
<div className="border-t border-gray-100 pt-4">
{!appliedPromo ? (
<>
<button
type="button"
onClick={() => setShowPromoField((v) => !v)}
className="text-xs text-gray-500 underline"
>
{showPromoField ? "Hide promo code" : "Have a promo code?"}
</button>
{showPromoField && (
<div className="mt-2 flex gap-2">
<input
value={promoCodeInput}
onChange={(e) => setPromoCodeInput(e.target.value)}
placeholder="Enter promo code"
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm"
/>
<button
type="button"
onClick={handleApplyPromo}
disabled={
applyingPromo || !promoCodeInput.trim() || !workspaceId
}
className="px-3 py-2 text-sm bg-gray-900 text-white rounded-lg disabled:opacity-50"
>
{applyingPromo ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
"Apply"
)}
</button>
</div>
)}
{promoError && (
<p className="text-xs text-red-500 mt-1">{promoError}</p>
)}
</>
) : (
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg text-sm">
<span className="text-green-700">
Promo applied: <strong>{appliedPromo.code}</strong> — you save{" "}
{(
appliedPromo.discountAmountKobo / 100
).toLocaleString(undefined, {
maximumFractionDigits: 2,
})}
</span>
<button
onClick={() => {
setAppliedPromo(null);
setPromoCodeInput("");
}}
className="text-xs text-gray-500 underline"
>
Remove
</button>
</div>
)}
</div>

<button
Expand Down
Loading