Fix early-bird price blending and mislabeling; add contact-only events

- Early-bird pricing: RegistrationOption now tracks each purchase as a
  separate price tranche instead of overwriting a single price/quantity
  on repeat purchases, so buying more tickets after a tier expires no
  longer re-prices tickets already bought at the old price. Stock-limit
  checks, total-due calculation, and the Finance report's revenue-by-
  option are all tranche-aware; pages that showed one blended price per
  line now render/total each tranche. Viewing a pending/partially-paid
  registration (dashboard, detail page, or an event's registration
  list) now refreshes stale pricing on the spot instead of only at
  payment time.
- Fixed the "(early bird)" dashboard label incorrectly firing on any
  line priced below the base option price (e.g. a plain cheaper
  variant) — it now checks the real applied-tier flag.
- Added contact-only events (e.g. baptism): no registration/payment
  flow, shown on the public site with a "Contact us" popup instead of
  a Register button. Configurable via the admin event wizard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:26:35 +02:00
co-authored by Claude Sonnet 5
parent f2c3172e16
commit f0f8d4c242
22 changed files with 804 additions and 225 deletions
+15
View File
@@ -2,6 +2,7 @@ import { notFound } from "next/navigation";
import { Navbar } from "@/components/layout/Navbar";
import { Footer } from "@/components/layout/Footer";
import ClientActions from "@/app/events/[id]/ClientActions";
import { ContactButton } from "@/components/events/ContactButton";
import { Calendar, Ticket, Paperclip, Sparkles } from "lucide-react";
export const revalidate = 60;
@@ -32,6 +33,10 @@ type Event = {
eventOptions?: EventOption[];
attachments?: EventAttachment[];
requiresAuth?: boolean;
requiresRegistration?: boolean;
contactName?: string | null;
contactPhone?: string | null;
contactEmail?: string | null;
};
import { apiFetch, ApiError } from "@/lib/api";
@@ -51,6 +56,16 @@ function lowStockThreshold(stockLimit: number): number {
}
function RegisterCta({ event }: { event: Event }) {
if (event.requiresRegistration === false) {
return (
<ContactButton
contactName={event.contactName}
contactPhone={event.contactPhone}
contactEmail={event.contactEmail}
className="block w-full text-center bg-brand-600 text-white px-4 py-2.5 rounded-lg hover:bg-brand-700 font-medium transition-colors"
/>
);
}
const now = new Date();
const end = new Date(event.endDate);
const deadline = event.registrationDeadline ? new Date(event.registrationDeadline) : null;