Skip to content

Instantly share code, notes, and snippets.

@NagariaHussain
Created August 13, 2025 14:04
Show Gist options
  • Select an option

  • Save NagariaHussain/9441284b0b35bae6a9d456c2de2f0580 to your computer and use it in GitHub Desktop.

Select an option

Save NagariaHussain/9441284b0b35bae6a9d456c2de2f0580 to your computer and use it in GitHub Desktop.
Frappe Builder | Events App Scripts
event_route = frappe.form_dict.route
event_doc = frappe.get_cached_doc("FE Event", {"route": event_route})


data.event_doc = event_doc


# Ticket Types
available_ticket_types = []
published_ticket_types = frappe.db.get_all(
    "Event Ticket Type", 
    filters={"is_published": True, "event": event_doc.name},
    pluck="name"
)
for ticket_type in published_ticket_types:
    tt = frappe.get_cached_doc("Event Ticket Type", ticket_type)
    print(tt.remaining_tickets)
    if tt.are_tickets_available(1):
        available_ticket_types.append(tt)
data.available_ticket_types = available_ticket_types

# Ticket Add-ons
add_ons = frappe.db.get_all(
    "Ticket Add-on", 
    filters={"event": event_doc.name}, 
    fields=["name", "title", "price", "currency", "user_selects_option", "options"]
)

for add_on in add_ons:
    if add_on.user_selects_option:
        add_on.options = add_on.options.split("\n")

data.available_add_ons = add_ons
document.addEventListener("alpine:init", () => {
  Alpine.store("booking_form", {
    // ... (all other properties like num_attendees, attendees, etc. remain the same) ...
    num_attendees: 1,
    attendees: [],
    available_add_ons: window.available_add_ons,
    available_ticket_types: window.available_ticket_types,
    add_ons_map: {},
    ticket_types_map: {},
    event_id: null,
    submitting: false, // To disable the button during request

    init() {
      // Create maps for quick data retrieval
      for (const add_on of this.available_add_ons) {
        this.add_ons_map[add_on.name] = add_on;
      }
      for (const tt of this.available_ticket_types) {
        this.ticket_types_map[tt.name] = tt;
      }
      
      if (this.available_ticket_types.length > 0) {
        this.event_id = this.available_ticket_types[0].event;
      }
      
      // Initialize the first attendee
      this.attendees.push(this.createNewAttendee());
    },

    createNewAttendee() {
      const new_attendee = {
        full_name: "",
        email: "",
        ticket_type: this.available_ticket_types[0]?.name || "",
        add_ons: {},
      };
      for (const add_on of this.available_add_ons) {
        new_attendee.add_ons[add_on.name] = {
          selected: false,
          option: add_on.options ? add_on.options[0] || null : null,
        };
      }
      return new_attendee;
    },

    // ... (summary and total getters remain the same) ...
    get summary() {
      const summary = {
        tickets: {},
        add_ons: {},
      };
      for (const attendee of this.attendees) {
        const ticket_type = attendee.ticket_type;
        if (ticket_type && this.ticket_types_map[ticket_type]) {
          if (!summary.tickets[ticket_type]) {
            summary.tickets[ticket_type] = {
              count: 0,
              amount: 0,
              price: this.ticket_types_map[ticket_type].price,
              title: this.ticket_types_map[ticket_type].title,
            };
          }
          summary.tickets[ticket_type].count++;
          summary.tickets[ticket_type].amount +=
            this.ticket_types_map[ticket_type].price;
        }
        for (const add_on_name in attendee.add_ons) {
          if (attendee.add_ons[add_on_name].selected) {
            if (!summary.add_ons[add_on_name]) {
              summary.add_ons[add_on_name] = {
                count: 0,
                amount: 0,
                price: this.add_ons_map[add_on_name].price,
                title: this.add_ons_map[add_on_name].title,
              };
            }
            summary.add_ons[add_on_name].count++;
            summary.add_ons[add_on_name].amount +=
              this.add_ons_map[add_on_name].price;
          }
        }
      }
      return summary;
    },

    get total() {
      let total = 0;
      const current_summary = this.summary;
      for (const key in current_summary.tickets) {
        total += current_summary.tickets[key].amount;
      }
      for (const key in current_summary.add_ons) {
        total += current_summary.add_ons[key].amount;
      }
      return total;
    },

    // MODIFIED submit method
    async submit() {
        if (this.submitting) return; // Prevent double-clicks
        this.submitting = true;
        
      // 1. Create a deep copy of the attendees to avoid modifying the UI state.
      const payload = JSON.parse(JSON.stringify(this.attendees));

      // 2. Transform the add_ons for each attendee.
      const attendees_payload = payload.map((attendee) => {
        const selected_add_ons = [];
        for (const add_on_name in attendee.add_ons) {
          const add_on_state = attendee.add_ons[add_on_name];
          if (add_on_state.selected) {
            selected_add_ons.push({
              add_on: add_on_name,
              // Use the selected option, or true if no option is applicable
              value: add_on_state.option || true,
            });
          }
        }
        // Replace the old add_ons object with the new, clean array
        attendee.add_ons = selected_add_ons;
        return attendee;
      });
      
      const final_payload = {
        event: this.event_id,
        attendees: attendees_payload,
        // Define the success URL for Frappe to redirect to
        redirect_to: "/booking-confirmation", // Or any other success page
      };
      
      try {

          const response = await fetch(
              "/api/v2/method/events.api.process_booking",
              {
                method: "POST",
                headers: {
                  "Content-Type": "application/json",
                  Accept: "application/json",
                  "X-Frappe-CSRF-Token": frappe.csrf_token,
                },
                body: JSON.stringify(final_payload),
              },
            );
            
            if (!response.ok) {
            // If the server returns an error (e.g., 4xx, 5xx)
              const errorData = await response.json();
              // Frappe often wraps errors in _server_messages
              const errorMessage =
                JSON.parse(errorData.errors || "[]")[0] ||
                "An unknown error occurred.";
              alert(errorMessage); // Show error to the user
            } else {
                const {data} = await response.json();
                window.location.href = data;
            }
      } catch(error) {
          console.error("Submission failed:", error);
            alert("Could not connect to the server. Please try again later.");
      } finally {
        // Re-enable the button regardless of success or failure
        this.submitting = false;
      }
    },
  });

  // ... (Alpine.effect remains the same) ...
  Alpine.effect(() => {
    const store = Alpine.store("booking_form");
    const target_count = store.num_attendees;
    const current_count = store.attendees.length;

    if (target_count > current_count) {
      for (let i = current_count; i < target_count; i++) {
        store.attendees.push(store.createNewAttendee());
      }
    } else if (target_count < current_count) {
      store.attendees.splice(target_count);
    }
  });
}); 
<script>
  // These are passed from your backend (e.g., Jinja2)
  window.available_add_ons = {{ available_add_ons|json }};
  window.available_ticket_types = {{ available_ticket_types|json }};
</script>

<div>
  <label>Num Attendees</label>
  <input
    min="1"
    x-model.number="$store.booking_form.num_attendees"
    type="number"
  />
</div>

<div>
  <template x-for="(attendee, index) in $store.booking_form.attendees" :key="index">
    <div class="attendee-card">
      <h4>Attendee <span x-text="index + 1"></span></h4>
      <input
        x-model="attendee.full_name"
        placeholder="Full Name"
        required
        type="text"
      />
      <input
        x-model="attendee.email"
        placeholder="Email"
        required
        type="email"
      />
      <div>
        <label>Ticket Type</label>
        <select x-model="attendee.ticket_type">
          {% for tt in available_ticket_types %}
          <option value="{{ tt.name }}">{{ tt.title }}</option>
          {% endfor %}
        </select>
      </div>
      <div>
        <h5>Add-ons</h5>
        {% for add_on in available_add_ons %}
        <div>
          <input
            type="checkbox"
            x-model="attendee.add_ons['{{add_on.name}}'].selected"
            :id="`add_on_{{add_on.name}}_${index}`"
          />
          <label :for="`add_on_{{add_on.name}}_${index}`"
            >{{ add_on.title }}</label
          >

          {% if add_on.user_selects_option %}
          <template x-if="attendee.add_ons['{{add_on.name}}'].selected">
            <div>
              <select x-model="attendee.add_ons['{{add_on.name}}'].option">
                {% for option in add_on.options %}
                <option value="{{ option }}">{{ option }}</option>
                {% endfor %}
              </select>
            </div>
          </template>
          {% endif %}
        </div>
        {% endfor %}
      </div>
    </div>
  </template>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment