diff --git a/buzz/events/doctype/buzz_event/buzz_event.js b/buzz/events/doctype/buzz_event/buzz_event.js
index cc4310b3..1dd1f137 100644
--- a/buzz/events/doctype/buzz_event/buzz_event.js
+++ b/buzz/events/doctype/buzz_event/buzz_event.js
@@ -1,5 +1,8 @@
-// Copyright (c) 2025, BWH Studios and contributors
-// For license information, please see license.txt
+// Copyright (c) 2025, BWH Studios
+
+/* ─────────────────────────────────────────────────────────────
+ FORM CONTROLLER
+ ───────────────────────────────────────────────────────────── */
const FIELD_LABELS = {
category: __("Category"),
@@ -311,21 +314,23 @@ frappe.ui.form.on("Buzz Event", {
frm.save();
});
- frm.set_query("track", "schedule", (doc, cdt, cdn) => {
- return {
- filters: {
- event: doc.name,
+ // Clone Event button – only shown on saved documents
+ if (!frm.is_new()) {
+ frm.add_custom_button(
+ __("Clone Event"),
+ () => {
+ show_clone_event_dialog(frm);
},
- };
+ __("Actions")
+ );
+ }
+
+ frm.set_query("track", "schedule", (doc, cdt, cdn) => {
+ return { filters: { event: doc.name } };
});
frm.set_query("default_ticket_type", (doc) => {
- return {
- filters: {
- event: doc.name,
- is_published: 1,
- },
- };
+ return { filters: { event: doc.name, is_published: 1 } };
});
// Save as Template button
@@ -361,7 +366,7 @@ frappe.ui.form.on("Buzz Event", {
method: "create_webinar_on_zoom",
btn,
freeze: true,
- }).then(({ message }) => {
+ }).then(() => {
frm.layout.tabs.find((t) => t.label == "Zoom Integration").set_active();
});
});
@@ -377,6 +382,625 @@ frappe.ui.form.on("Buzz Event", {
},
});
+/* ─────────────────────────────────────────────────────────────────────────────
+ SHARED HELPERS
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/** Format "HH:MM:SS" → "HH : MM AM/PM" */
+function buzz_fmt_time(t) {
+ if (!t) return "—";
+ const [hh, mm] = t.split(":");
+ let h = parseInt(hh);
+ const ampm = h >= 12 ? "PM" : "AM";
+ h = h % 12 || 12;
+ return `${String(h).padStart(2, "0")} : ${mm || "00"} ${ampm}`;
+}
+
+/** Format "YYYY-MM-DD" → "Mon, Mar 3" */
+function buzz_fmt_date(d) {
+ if (!d) return "—";
+ return frappe.datetime
+ .str_to_obj(d)
+ .toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ COMPUTE OCCURRENCES (pure JS, no library)
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/**
+ * @param {string} start_date "YYYY-MM-DD"
+ * @param {string} start_time "HH:MM:SS"
+ * @param {string} repeat_type "Daily" | "Weekly" | "Monthly"
+ * @param {number[]} weekdays ISO indices 0=Mon … 6=Sun (only used for Weekly)
+ * @param {string} end_type "Until" | "For"
+ * @param {string} until_date "YYYY-MM-DD" (used when end_type === "Until")
+ * @param {number} for_count integer (used when end_type === "For")
+ */
+function buzz_compute_occurrences(
+ start_date,
+ start_time,
+ repeat_type,
+ weekdays,
+ end_type,
+ until_date,
+ for_count
+) {
+ const SAFETY = 500;
+ const results = [];
+ const cur = frappe.datetime.str_to_obj(start_date);
+
+ // Compute the end fence date
+ let limit_date = null;
+ if (end_type === "Until" && until_date) {
+ limit_date = frappe.datetime.str_to_obj(until_date);
+ } else if (end_type === "For") {
+ const n = Math.max(1, parseInt(for_count) || 1);
+ limit_date = new Date(cur);
+ if (repeat_type === "Daily") {
+ limit_date.setDate(limit_date.getDate() + n - 1);
+ } else if (repeat_type === "Weekly") {
+ limit_date.setDate(limit_date.getDate() + n * 7 - 1);
+ } else if (repeat_type === "Monthly") {
+ limit_date.setMonth(limit_date.getMonth() + n);
+ limit_date.setDate(limit_date.getDate() - 1);
+ }
+ }
+
+ // Hard fence: two years out
+ const fence = new Date(cur);
+ fence.setFullYear(fence.getFullYear() + 2);
+
+ let iters = 0;
+
+ while (iters < SAFETY) {
+ iters++;
+ if (cur > fence) break;
+ if (limit_date && cur > limit_date) break;
+
+ const date_str = frappe.datetime.obj_to_str(cur);
+
+ if (repeat_type === "Daily") {
+ results.push({ start_date: date_str, start_time });
+ cur.setDate(cur.getDate() + 1);
+ } else if (repeat_type === "Weekly") {
+ // JS getDay(): 0=Sun … 6=Sat → convert to ISO 0=Mon … 6=Sun
+ const js_day = cur.getDay();
+ const iso_day = js_day === 0 ? 6 : js_day - 1;
+ if (weekdays.includes(iso_day)) {
+ results.push({ start_date: date_str, start_time });
+ }
+ cur.setDate(cur.getDate() + 1);
+ } else if (repeat_type === "Monthly") {
+ results.push({ start_date: date_str, start_time });
+ cur.setMonth(cur.getMonth() + 1);
+ }
+ }
+
+ return results;
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ CLONE EVENT DIALOG
+ ───────────────────────────────────────────────────────────────────────────── */
+
+function show_clone_event_dialog(frm) {
+ frappe.call({
+ method: "buzz.events.doctype.buzz_event.buzz_event.get_clone_event_dialog_html",
+ args: { context: JSON.stringify({ title: frm.doc.title }) },
+ callback(r) {
+ _build_clone_dialog(frm, r.message);
+ },
+ });
+}
+
+function _build_clone_dialog(frm, body_html) {
+ let selected_dates = [];
+
+ // Render the date-rows list inside #clone-dates-list
+ function render_dates() {
+ const $list = clone_dialog.$body.find("#clone-dates-list");
+ $list.empty();
+
+ if (!selected_dates.length) {
+ $list.html(
+ `
+ ${__("No times added yet.")}
+
`
+ );
+ return;
+ }
+
+ selected_dates.forEach((entry, idx) => {
+ const $row = $(`
+
+ `);
+ $list.append($row);
+
+ // Date control
+ const date_ctrl = frappe.ui.form.make_control({
+ df: { fieldtype: "Date", fieldname: `row_date_${idx}`, label: "" },
+ parent: $row.find(".row-date-wrap")[0],
+ render_input: true,
+ });
+ date_ctrl.refresh();
+ $row.find(".row-date-wrap .frappe-control").css({ margin: 0, padding: 0 });
+ $row.find(".row-date-wrap .form-group").css({ margin: 0 });
+ $row.find(".row-date-wrap label").hide();
+ date_ctrl.set_value(entry.start_date || "");
+ date_ctrl.$input.on("change blur", () => {
+ const v = date_ctrl.get_value();
+ if (v) selected_dates[idx].start_date = v;
+ sync_primary_label();
+ });
+
+ // Time control
+ const time_ctrl = frappe.ui.form.make_control({
+ df: { fieldtype: "Time", fieldname: `row_time_${idx}`, label: "" },
+ parent: $row.find(".row-time-wrap")[0],
+ render_input: true,
+ });
+ time_ctrl.refresh();
+ $row.find(".row-time-wrap .frappe-control").css({ margin: 0, padding: 0 });
+ $row.find(".row-time-wrap .form-group").css({ margin: 0 });
+ $row.find(".row-time-wrap label").hide();
+ time_ctrl.set_value(entry.start_time || "");
+ time_ctrl.$input.on("change blur", () => {
+ const v = time_ctrl.get_value();
+ if (v) selected_dates[idx].start_time = v;
+ });
+
+ // Remove
+ $row.find(".row-remove-btn").on("click", () => {
+ selected_dates.splice(idx, 1);
+ render_dates();
+ sync_primary_label();
+ });
+ });
+ }
+
+ function sync_primary_label() {
+ const n = selected_dates.length;
+ clone_dialog.set_primary_action(
+ n > 0 ? __("Clone {0} Event(s)", [n]) : __("Clone Event"),
+ null
+ );
+ }
+
+ const clone_dialog = new frappe.ui.Dialog({
+ title: __("Clone Event"),
+ fields: [
+ {
+ fieldname: "body_html",
+ fieldtype: "HTML",
+ options: body_html,
+ },
+ {
+ label: __("Host"),
+ fieldname: "host",
+ fieldtype: "Link",
+ options: "Event Host",
+ default: frm.doc.host,
+ reqd: 1,
+ },
+ ],
+ primary_action_label: __("Clone Event"),
+ primary_action(values) {
+ if (!selected_dates.length) {
+ frappe.msgprint({
+ message: __("Please add at least one date."),
+ indicator: "orange",
+ });
+ return;
+ }
+ clone_dialog.disable_primary_action();
+ frappe.call({
+ method: "buzz.events.doctype.buzz_event.buzz_event.clone_buzz_event",
+ args: { name: frm.doc.name, dates: selected_dates, host: values.host },
+ callback(r) {
+ clone_dialog.hide();
+ if (r.message && r.message.length) {
+ const links = r.message
+ .map((n) => `#${n}`)
+ .join(", ");
+ frappe.msgprint({
+ title: __("Events Created"),
+ message: __("{0} Buzz Event(s) created: {1}", [
+ r.message.length,
+ links,
+ ]),
+ indicator: "green",
+ });
+ }
+ },
+ error() {
+ clone_dialog.enable_primary_action();
+ },
+ });
+ },
+ });
+
+ clone_dialog.show();
+ render_dates();
+
+ const $host_field = clone_dialog.fields_dict.host.$wrapper;
+ const $body_html = clone_dialog.$body.find('[data-fieldname="body_html"]');
+ $body_html.before($host_field.detach());
+
+ clone_dialog.$body.on("click", "#clone-add-time-btn", () => {
+ show_add_time_dialog(
+ frm.doc.start_date || frappe.datetime.get_today(),
+ frm.doc.start_time || "15:00:00",
+ (entry) => {
+ selected_dates.push(entry);
+ render_dates();
+ sync_primary_label();
+ }
+ );
+ });
+
+ clone_dialog.$body.on("click", "#clone-recurrence-btn", () => {
+ show_recurrence_dialog(
+ frm.doc.start_date || frappe.datetime.get_today(),
+ frm.doc.start_time || "15:00:00",
+ (entries) => {
+ selected_dates = selected_dates.concat(entries);
+ render_dates();
+ sync_primary_label();
+ }
+ );
+ });
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ ADD SINGLE TIME DIALOG
+ ───────────────────────────────────────────────────────────────────────────── */
+
+function show_add_time_dialog(default_date, default_time, on_add) {
+ const d = new frappe.ui.Dialog({
+ title: __("Add Time"),
+ fields: [
+ {
+ label: __("Date"),
+ fieldname: "start_date",
+ fieldtype: "Date",
+ reqd: 1,
+ default: default_date,
+ },
+ {
+ label: __("Time"),
+ fieldname: "start_time",
+ fieldtype: "Time",
+ default: default_time,
+ },
+ ],
+ primary_action_label: __("Add"),
+ primary_action(values) {
+ on_add({ start_date: values.start_date, start_time: values.start_time });
+ d.hide();
+ },
+ });
+ d.show();
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ CHOOSE TIMES (RECURRENCE DIALOG)
+ ───────────────────────────────────────────────────────────────────────────── */
+
+function show_recurrence_dialog(default_date, default_time, on_add) {
+ frappe.call({
+ method: "buzz.events.doctype.buzz_event.buzz_event.get_recurrence_dialog_html",
+ callback(r) {
+ _build_recurrence_dialog(default_date, default_time, on_add, r.message);
+ },
+ });
+}
+
+function _build_recurrence_dialog(default_date, default_time, on_add, body_html) {
+ // ISO weekday index: 0=Mon … 6=Sun
+ const DAY_META = [
+ { idx: 0, label: "M", full: "Monday" },
+ { idx: 1, label: "T", full: "Tuesday" },
+ { idx: 2, label: "W", full: "Wednesday" },
+ { idx: 3, label: "T", full: "Thursday" },
+ { idx: 4, label: "F", full: "Friday" },
+ { idx: 5, label: "S", full: "Saturday" },
+ { idx: 6, label: "S", full: "Sunday" },
+ ];
+
+ // Pre-select the ISO weekday of the starting date
+ const init_obj = frappe.datetime.str_to_obj(default_date);
+ const js_day = init_obj.getDay(); // 0=Sun
+ let selected_weekdays = [js_day === 0 ? 6 : js_day - 1];
+
+ // JS-owned state for the end conditions (not Frappe fields)
+ let current_end_type = "For"; // "Until" | "For"
+ let until_date_state = ""; // set by ControlDate
+ let for_count_state = 5; // set by number input
+ // Keep a reference to the live ControlDate so we can read it in refresh_preview
+ let until_date_ctrl = null;
+
+ const recurrence_dialog = new frappe.ui.Dialog({
+ title: __("Choose Times"),
+ fields: [
+ {
+ label: __("Starting on"),
+ fieldname: "start_date",
+ fieldtype: "Date",
+ reqd: 1,
+ default: default_date,
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Time"),
+ fieldname: "start_time",
+ fieldtype: "Time",
+ default: default_time,
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Repeats"),
+ fieldname: "repeat_type",
+ fieldtype: "Select",
+ options: ["Daily", "Weekly", "Monthly"].join("\n"),
+ default: "Weekly",
+ reqd: 1,
+ },
+ {
+ fieldname: "recurrence_body",
+ fieldtype: "HTML",
+ options: body_html,
+ },
+ ],
+ primary_action_label: __("Add Times"),
+ primary_action(values) {
+ // Read until_date from our ctrl if it exists
+ if (current_end_type === "Until" && until_date_ctrl) {
+ until_date_state = until_date_ctrl.get_value() || until_date_state;
+ }
+ const occurrences = buzz_compute_occurrences(
+ values.start_date,
+ values.start_time || default_time,
+ values.repeat_type,
+ selected_weekdays,
+ current_end_type,
+ until_date_state,
+ for_count_state
+ );
+ if (!occurrences.length) {
+ frappe.msgprint({
+ message: __("No dates generated. Check your recurrence settings."),
+ indicator: "orange",
+ });
+ return;
+ }
+ on_add(occurrences);
+ recurrence_dialog.hide();
+ },
+ });
+
+ recurrence_dialog.show();
+
+ /* ── helpers ── */
+
+ function get_repeat_type() {
+ return recurrence_dialog.get_field("repeat_type").get_value() || "Weekly";
+ }
+
+ function get_start_date() {
+ return recurrence_dialog.get_field("start_date").get_value() || default_date;
+ }
+
+ function get_start_time() {
+ return recurrence_dialog.get_field("start_time").get_value() || default_time;
+ }
+
+ /* ── Weekday button styles ── */
+ function render_weekday_buttons() {
+ recurrence_dialog.$body.find(".recurrence-day-btn").each(function () {
+ const day = parseInt($(this).data("day"));
+ const active = selected_weekdays.includes(day);
+ $(this)
+ .toggleClass("btn-primary", active)
+ .toggleClass("btn-default", !active)
+ .css("border", "");
+ });
+ }
+
+ /* ── Show/hide weekday section ── */
+ function toggle_weekday_section() {
+ const $s = recurrence_dialog.$body.find("#recurrence-weekday-section");
+ get_repeat_type() === "Weekly" ? $s.show() : $s.hide();
+ }
+
+ /* ── End condition: "Until" date or "For N units" ── */
+ function render_end_condition() {
+ const $wrapper = recurrence_dialog.$body.find("#end-condition-wrapper");
+ $wrapper.empty();
+ until_date_ctrl = null;
+
+ // Update toggle button active state using Frappe classes
+ recurrence_dialog.$body.find(".end-type-btn").each(function () {
+ const active = $(this).data("type") === current_end_type;
+ $(this).toggleClass("btn-primary", active).toggleClass("btn-default", !active);
+ });
+
+ if (current_end_type === "Until") {
+ // Use Frappe's ControlDate rendered directly into the wrapper
+ until_date_ctrl = frappe.ui.form.make_control({
+ df: {
+ fieldtype: "Date",
+ fieldname: "recurrence_until_date",
+ label: "",
+ placeholder: __("End date"),
+ },
+ parent: $wrapper[0],
+ render_input: true,
+ });
+ until_date_ctrl.refresh();
+ // Strip extra spacing that make_control adds so it sits flush inline
+ $wrapper.find(".frappe-control").css({ margin: "0", padding: "0" });
+ $wrapper.find(".form-group").css({ margin: "0" });
+ $wrapper.find(".frappe-control label").hide();
+ // Restore previously chosen date
+ if (until_date_state) {
+ until_date_ctrl.set_value(until_date_state);
+ }
+ // Wire change
+ until_date_ctrl.$input.on("change blur", function () {
+ until_date_state = until_date_ctrl.get_value() || "";
+ refresh_preview();
+ });
+ } else {
+ // "For N weeks/days/months"
+ const unit_map = {
+ Daily: __("days"),
+ Weekly: __("weeks"),
+ Monthly: __("months"),
+ };
+ const unit = unit_map[get_repeat_type()] || __("occurrences");
+
+ $wrapper.html(`
+
+ ${unit}
+ `);
+
+ $wrapper.find("#end-for-count").on("input change", function () {
+ for_count_state = Math.max(1, parseInt($(this).val()) || 1);
+ // Keep the input aligned with state
+ $(this).val(for_count_state);
+ refresh_preview();
+ });
+ }
+ }
+
+ /* ── Occurrence preview chips ── */
+ function refresh_preview() {
+ const start_date = get_start_date();
+ const start_time = get_start_time();
+ const repeat_type = get_repeat_type();
+
+ if (!start_date) return;
+
+ // If Until mode, try to read the latest value from the live ctrl
+ let effective_until = until_date_state;
+ if (current_end_type === "Until" && until_date_ctrl) {
+ effective_until = until_date_ctrl.get_value() || until_date_state;
+ }
+
+ const occurrences = buzz_compute_occurrences(
+ start_date,
+ start_time,
+ repeat_type,
+ selected_weekdays,
+ current_end_type,
+ effective_until,
+ for_count_state
+ );
+
+ const $preview = recurrence_dialog.$body.find("#recurrence-preview");
+ $preview.empty();
+
+ if (!occurrences.length) {
+ $preview.html(
+ `
+ ${__("No dates match these settings.")}
+ `
+ );
+ recurrence_dialog.set_primary_action(__("Add Times"), null);
+ return;
+ }
+
+ const MAX_CHIPS = 4;
+ const visible = occurrences.slice(0, MAX_CHIPS);
+ const overflow = occurrences.length - MAX_CHIPS;
+
+ visible.forEach((entry) => {
+ const d_obj = frappe.datetime.str_to_obj(entry.start_date);
+ $preview.append(`
+
+
+ ${d_obj.toLocaleDateString("en-US", { month: "short" })}
+
+
+ ${d_obj.getDate()}
+
+
+ ${d_obj.toLocaleDateString("en-US", { weekday: "short" }).toUpperCase()}
+
+
+ `);
+ });
+
+ if (overflow > 0) {
+ $preview.append(`
+
+ +${overflow}
+
+ `);
+ }
+
+ recurrence_dialog.set_primary_action(__("Add {0} Time(s)", [occurrences.length]), null);
+ }
+
+ /* ── Initial render ── */
+ render_weekday_buttons();
+ toggle_weekday_section();
+ render_end_condition();
+ refresh_preview();
+
+ /* ── Event bindings ── */
+
+ // Weekday toggle
+ recurrence_dialog.$body.on("click", ".recurrence-day-btn", function () {
+ const day = parseInt($(this).data("day"));
+ const idx = selected_weekdays.indexOf(day);
+ if (idx === -1) {
+ selected_weekdays.push(day);
+ } else if (selected_weekdays.length > 1) {
+ selected_weekdays.splice(idx, 1); // keep at least one day selected
+ }
+ render_weekday_buttons();
+ refresh_preview();
+ });
+
+ // Until / For toggle
+ recurrence_dialog.$body.on("click", ".end-type-btn", function () {
+ const type = $(this).data("type");
+ if (current_end_type === type) return;
+ current_end_type = type;
+ render_end_condition();
+ refresh_preview();
+ });
+
+ // Repeat type change
+ recurrence_dialog.get_field("repeat_type").$input.on("change", function () {
+ toggle_weekday_section();
+ render_end_condition(); // update "weeks/days/months" unit label in For mode
+ refresh_preview();
+ });
+
+ // Start date / time changes
+ recurrence_dialog.get_field("start_date").$input.on("change", () => refresh_preview());
+ recurrence_dialog.get_field("start_time").$input.on("change", () => refresh_preview());
+}
function getZoomSupportedTimezones() {
return [
"Pacific/Midway",
diff --git a/buzz/events/doctype/buzz_event/buzz_event.py b/buzz/events/doctype/buzz_event/buzz_event.py
index f276693a..e7785dcd 100644
--- a/buzz/events/doctype/buzz_event/buzz_event.py
+++ b/buzz/events/doctype/buzz_event/buzz_event.py
@@ -213,6 +213,90 @@ def update_zoom_webinar(self):
webinar.save()
+@frappe.whitelist()
+def get_clone_event_dialog_html(context: str | dict | None = None) -> str:
+ """Render the clone-event dialog template and return the HTML string."""
+ import json
+
+ ctx = {}
+ if context:
+ ctx = json.loads(context) if isinstance(context, str) else context
+
+ return frappe.render_template("buzz/templates/clone_event_dialog/clone_event_dialog.html", ctx)
+
+
+@frappe.whitelist()
+def get_recurrence_dialog_html() -> str:
+ """Render the recurrence dialog template and return the HTML string."""
+ return frappe.render_template("buzz/templates/clone_event_dialog/recurrence_dialog.html", {})
+
+
+@frappe.whitelist()
+def clone_buzz_event(name: str, dates: str | list, host: str | None = None) -> list[str]:
+ """
+ Clone a Buzz Event for each entry in `dates`.
+
+ Args:
+ name : name of the source Buzz Event
+ dates : JSON list of {"start_date": "YYYY-MM-DD", "start_time": "HH:MM:SS"}
+ host : optional Event Host to override on clones
+
+ Returns:
+ list of newly created Buzz Event names
+ """
+ import json
+
+ from frappe.utils import add_days, date_diff
+ from frappe.utils.data import get_time_str
+
+ if isinstance(dates, str):
+ dates = json.loads(dates)
+
+ if not dates:
+ frappe.throw(frappe._("Please provide at least one date."))
+
+ source = frappe.get_doc("Buzz Event", name)
+
+ # Preserve the original duration (end_date - start_date offset in days)
+ duration_days = 0
+ if source.end_date and source.start_date:
+ duration_days = date_diff(source.end_date, source.start_date)
+
+ created = []
+
+ for entry in dates:
+ new_doc = frappe.copy_doc(source)
+ new_doc.start_date = entry.get("start_date")
+ new_doc.start_time = entry.get("start_time") or source.start_time
+
+ new_doc.start_time = get_time_str(new_doc.start_time)
+ new_doc.end_time = get_time_str(new_doc.end_time)
+
+ if duration_days:
+ new_doc.end_date = add_days(new_doc.start_date, duration_days)
+ else:
+ new_doc.end_date = new_doc.start_date
+
+ # New clone starts as a draft
+ new_doc.is_published = 0
+ new_doc.route = ""
+
+ if host:
+ new_doc.host = host
+
+ # Adjust schedule item dates by the same offset
+ if source.start_date and new_doc.schedule:
+ for row in new_doc.schedule:
+ if row.date:
+ row_offset = date_diff(row.date, source.start_date)
+ row.date = add_days(new_doc.start_date, row_offset)
+
+ new_doc.insert()
+ created.append(new_doc.name)
+
+ return created
+
+
@frappe.whitelist()
def create_from_template(template_name: str, options: str, additional_fields: str = "{}") -> str:
"""
diff --git a/buzz/templates/clone_event_dialog/clone_event_dialog.html b/buzz/templates/clone_event_dialog/clone_event_dialog.html
new file mode 100644
index 00000000..2b87d753
--- /dev/null
+++ b/buzz/templates/clone_event_dialog/clone_event_dialog.html
@@ -0,0 +1,47 @@
+
+
+
+ {{ _("New Times") }}
+
+
+
+
+
+
+
+
diff --git a/buzz/templates/clone_event_dialog/recurrence_dialog.html b/buzz/templates/clone_event_dialog/recurrence_dialog.html
new file mode 100644
index 00000000..7e7f020c
--- /dev/null
+++ b/buzz/templates/clone_event_dialog/recurrence_dialog.html
@@ -0,0 +1,47 @@
+
+
+
+ {{ _("Days of the week") }}
+
+
+ {% set day_labels = [("M","Monday"),("T","Tuesday"),("W","Wednesday"),("T","Thursday"),("F","Friday"),("S","Saturday"),("S","Sunday")] %}
+ {% for label, full in day_labels %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ _("Set options above to preview dates…") }}
+
+
+